Springboot2核心技术
SpringBoot是整合Spring技术栈的一站式框架SpringBoot是简化Spring技术栈的快速开发脚手架当然springboot也有一些缺点:版本迭代快,需要时刻关注变化。封装太深,内部原理复杂,不容易精通James Lewis and Martin Fowler (2014)提出微服务完整概念。
Springboot2核心技术
1 springboot2–基础入门
1.1 spring与springboot
1.1.1 spring能为我们做什么?
我们可以查看官网:https://spring.io/
1.1.2 spring的生态
https://spring.io/projects/spring-boot
1.1.3 为什么要用springboot
能快速创建出生产级别的Spring应用。
1.1.4 springboot的优点
-
- 创建独立Spring应用
- 内嵌web服务器
- 自动starter依赖,简化构建配置
- 自动配置Spring以及第三方功能
- 提供生产级别的监控、健康检查及外部化配置
- 无代码生成、无需编写XML
总结:
SpringBoot是整合Spring技术栈的一站式框架
SpringBoot是简化Spring技术栈的快速开发脚手架
当然springboot也有一些缺点:
-
版本迭代快,需要时刻关注变化。
-
封装太深,内部原理复杂,不容易精通
1.2 springboot诞生的背景
1.2.1 微服务
James Lewis and Martin Fowler (2014) 提出微服务完整概念。https://martinfowler.com/microservices/
In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.-- James Lewis and Martin Fowler (2014)
简单的来说:
- 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 每个服务运行在自己的进程内,也就是可独立部署和升级
- 服务之间使用轻量级HTTP交互
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
1.2.2 分布式
一旦我们将大型应用拆分成各个小的微服务之后,必然会产生分布式。
那么分布式也会产生各种问题:
- 远程调用
- 服务发现
- 负载均衡
- 服务容错
- 配置管理
- 服务监控
- 链路追踪
- 日志管理
- 任务调度
分布式问题的解决:
使用springboot快速的构建应用,由于微服务模块众多,我们使用springcloud来解决微服务模块众多带来的问题。由于微服务架构下会产生大量数据,我们可以使用springcloud data flow做成响应式数据流来整合起来。
1.3 如何学习springboot
1.3.1 官方文档架构
当然我们也可以去查看版本新特性;
https://github.com/spring-projects/spring-boot/wiki#release-notes
比如我们可以查看2.0版本的新特性。https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Release-Notes
1.4 springboot入门案例
我们搭建这个入门案例会参照官方文档进行搭建。
系统要求:
-
jdk 1.8
-
maven 3.5+
1.4.1 搭建hello案例
需求:浏览发送/hello请求,响应 HelloWorld,Spring Boot 2 。
1.4.1.1 创建maven工程
使用maven创建java工程,不用使用骨架创建。
1.4.1.2 引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
1.4.1.3 创建主程序
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
1.4.1.4 编写业务
@RestController
public class HelloController {
@RequestMapping("hello")
public String hello(){
return "hello" + "你好";
}
}
最后定义项目目录结构如下:
1.4.1.5 测试
直接运行main方法
执行:http://localhost:8888/hello
1.4.1.6 定义配置文件
定义配置文件application.properties。
server.port=8888
更多详细配置,详情参考官方文档。
1.4.1.7 部署项目
我们还是参照官方文档,关于springboot项目的部署,官方文档做了如下定义:
在pom文件里面,我们做以上定义。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
把项目打成jar包,直接在目标服务器执行即可。
执行打包命令之后,target目录会产生一个jar包。
进入target目录 通过java -jar的命令运行这个jar包。
执行:http://localhost:8888/hello会有同样的效果。
注意点:
- 如果运行不成功,请取消掉cmd的快速编辑模式。在cmd窗口,右键->属性 即可设置。
1.5 springboot自动配置原理
1.5.1 springboot依赖管理
我们在pom.xml里面配置了一个父依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
这个依赖有什么作用?
学过maven都知道,父依赖就是进行依赖管理的。父工程里面会定义大量的依赖,并且对这些依赖进行了版本控制。那么在springboot里面是不是这样子的呢?
我们点击spring-boot-starter-parent查看具体情况:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.0.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
点击进去,我们发现spring-boot-starter-parent还有一个父项目,就是spring-boot-dependencies。那么这个依赖里面到底做了哪些事情?
我们继续点击spring-boot-dependencies进去查看:
我们发现这个工程对大量依赖做了管理,并进行了版本控制:
所以我们可以得出这样一个结论:spring-boot-starter-parent的作用就是声明了所有开发中常用的依赖的版本号,自动版本仲裁机制。
比如我们最常用的mysql数据库驱动版本,在这里就做了版本控制:
此时我们可以在pom.xml里面引入mysql数据库驱动依赖,来验证这个版本是否正确。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
由此可见:这个父依赖spring-boot-starter-parent确实给我们做了依赖的版本管理。但是有的时候springboot官方给我们仲裁的依赖版本并不一定合理。
比如这个mysql-connector-java依赖,官方定义的是8的版本,要求我们本地数据库也必须是mysql8的版本。如果我们自己的数据库是mysql5.x的版本,此时springboot官方给我仲裁的mysql驱动版本明显就不合适了。此时我们需要修改springboot给我们mysql仲裁的版本。如何修改?
我们只需要在pom.xml里面自定义mysql驱动的版本号即可。
<properties>
<mysql.version>5.1.6</mysql.version>
</properties>
修改之后,我们查看我们依赖的版本号:
我们发现自己定义的版本生效了。
在pom.xml里面,我们还定义了spring-boot-starter-web这个依赖。我们称之为web启动器。那么什么是启动器?我们看官方文档给我们作的描述。
我们可以看看官方定义的启动器:
所以我们pom.xml里面导入spring-boot-starter-web启动器的作用就是引入了一系列web场景的依赖。没有springboot之前,这些依赖都需要我们自己手动导入。但是有了springboot之后,通过这个web启动器,我们就可以把这个场景需要的一系列的依赖全部导入进来。
我们可以点击进入spring-boot-starter-web启动器。
注意:所有的启动器,底层必须依赖spring-boot-starter。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.0.RELEASE</version>
<scope>compile</scope>
</dependency>
如果官方定义的starter不满足我们的需求,我们也可以自定义starter。这一点,官方文档里面也做了明确的阐述。
比如mybatis启动器:mybatis-spring-boot-starter。
1.5.2 自动配置
我们以我们引入的spring-boot-starter-web启动器为例。当我们点击打开spring-boot-starter-web启动器,我们发现它包含很多其他的启动器。比如:
json相关的配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.2.0.RELEASE</version>
<scope>compile</scope>
</dependency>
tomcat相关的配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.2.0.RELEASE</version>
<scope>compile</scope>
</dependency>
springmvc相关的配置
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
<scope>compile</scope>
</dependency>
我们认为通过引入web启动器,在springboot项目加载的时候,就会对springmvc 、json 、tomcat等进行自动配置。我们可以验证一下:
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
//获取所有bean的名称的集合
String[] beanDefinitionNames = context.getBeanDefinitionNames();
//循环获取bean的名称
for(String name : beanDefinitionNames){
System.out.println(name);
}
}
}
... ...
org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
helloController
... ...
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat
tomcatServletWebServerFactory
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
servletWebServerFactoryCustomizer
tomcatServletWebServerFactoryCustomizer
server-org.springframework.boot.autoconfigure.web.ServerProperties
webServerFactoryCustomizerBeanPostProcessor
errorPageRegistrarBeanPostProcessor
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration
dispatcherServlet --前端控制器
... ...
mvcHandlerMappingIntrospector
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter
defaultViewResolver
viewResolver -- 视图解析器
requestContextFilter
... ...
characterEncodingFilter -- 编码过滤器
localeCharsetMappingsCustomizer
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
multipartConfigElement
multipartResolver --文件上传解析器
spring.servlet.multipart-org.springframework.boot.autoconfigure.web.servlet.MultipartProperties
我们大致发现,之前我们配置的比如:前端控制器、视图解析器、编码过滤器、文件上传解析器等都装配进了ioc容器,换句话也就是说进行了自动配置。而这些在我们之前ssm整合的时候,都是需要我们自己去手动配置的。
此时大家应该还有疑问,就是我们之前在进行SSM整合的时候,我们都会配置包扫描的规则。
<context:component-scan base-package="com.xq.controller">
或者使用@ComponentScan注解。
但是在springboot中,已经默认给我们规定好了包扫描规则,那就是主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来。
我们可以测试一下:
首先我修改项目的目录结构:
我们再启动项目测试:
由此可见,当目录发生变化之后,controller类没有被扫描到spring ioc容器,故而找不到这个bean,进而报404异常。
当然,我们也可以自定义包扫描规则:
@SpringBootApplication(scanBasePackages = "com.controller") //自定义包扫描规则
public class App{}
再测试,发现没有问题
虽然可以自定义包的扫描规则。但是我们一般不这么去做,还是尽量遵循springboot官方给我们定义的默认包扫描规则。
springboot的各种自动配置都是有默认值的。我们以文件上传的配置为例:
默认配置最终绑定到某个类上面,比如:MultipartProperties。配置文件的值最终绑定到某个类的属性上面,这个类所属的bean要交给spring容器管理。
springboot虽然会自动配置,但是不会将所有场景的资源都进行自动配置。它采用的是按需加载的的方式来进行自动配置的。我们引入什么启动器,就会针对这个启动器的场景进行自动配置。
SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面。
我们随便点击一个启动器进去,就可以发现它。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.0.RELEASE</version>
<scope>compile</scope>
</dependency>
我们点击这个启动器再次进入
这个spring-boot-autoconfigure是什么呢?我们可以去项目里面查看一下:
但是这些场景的资源不会全部自动加载。只有引入对应的启动器,才会按需加载。
比如我在pom.xml里面引入一个rabbitmq的启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
我们在重新启动启动器,查看控制台打印输出:
1.6 springboot常用注解
1.6.1 组件添加的注解
1.6.1.1 @Configuration注解
以前,如果我们想将bean交给spring容器管理,我们会怎么办? 我们可以使用xml或者注解的方式进配置。在springboot中还有一种非常常见的方式管理bean。那就是使用配置类的方式管理bean。
现在我们就使用java配置类的方式管理bean。
- 定义两个pojo类
public class User {
private String username;
private Integer age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}
public class Cat {
private String name;
private String color;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
- 使用java配置类来管理
@Configuration
public class MyConfig {
@Bean
public User user(){
User user = new User();
user.setUsername("eric");
user.setAge(18);
return user;
}
@Bean
public Cat cat(){
Cat cat = new Cat();
cat.setName("大橘");
cat.setColor("橘色");
return cat;
}
}
释义:
@Configuration: 告诉SpringBoot这是一个配置类,等同于原生spring项目的配置文件的作用。
@Bean:向spring容器中添加组件。类似于原生spring中的xml配置文件。方法名相当于bean的id。方法返回的值,就是组件在容器中的实例。
我们的组件到底有没有注入到spring ioc容器?我们可以测试一下:
我们运行启动器:
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
//获取所有bean的名称的集合
String[] beanDefinitionNames = context.getBeanDefinitionNames();
//循环获取bean的名称
for(String name : beanDefinitionNames){
System.out.println(name);
}
}
}
观察控制台:
现在我们继续测试:
我们在启动类App中添加如下代码:
User user01 = context.getBean("user", User.class);
User user02 = context.getBean("user", User.class);
System.out.println("组件:"+(user01 == user02));
运行App,运行结果如下:
组件:true
我们可以得知:我们获取的组件就是从spring容器中获取的,并且是单实例的bean。
其实我们配置类也是一个组件,也是交给spring容器管理了。我们点击@Configuration注解,进入查看源码发现:
我们可以尝试获取这个bean
MyConfig myConfig = context.getBean(MyConfig.class);
System.out.println(myConfig);
输出结果是:
com.xq.config.MyConfig$$EnhancerBySpringCGLIB$$6064970@3e681bc
观察可知这是一个基于Cglib字节码增强的代理对象!!!也就是在spring容器里面管理的是这个配置类的代理对象(增强的对象)。
其实@Configuration注解还有一个非常重要的属性,就是proxyBeanMethods。
@Configuration(proxyBeanMethods = true)
这个值默认为true。
我们现在来测试这个属性的作用。我们先在启动类中添加如下代码。
User user1 = myConfig.user();
User user2 = myConfig.user();
System.out.println(user1 == user2);
结果是:true。
为什么为true。其实就是通过MyConfig在spring容器中的代理对象调用方法。SpringBoot总会检查这个组件是否在容器中存在。
我们把这个属性值改成false。
@Configuration(proxyBeanMethods = false)
再次运行上面的代码。
结果是:false。
这就是每个@Bean方法被调用多少次返回的组件都是新创建的。
这个proxyBeanMethods属性其实就是解决一个问题,那就是组件依赖。
我们现在恢复成:@Configuration(proxyBeanMethods = true)
public class User {
private String username;
private Integer age;
private Cat cat;
//省略get set 方法
}
@Configuration(proxyBeanMethods = true)
public class MyConfig {
@Bean
public User user(){
User user = new User();
user.setUsername("eric");
user.setAge(18);
user.setCat(cat());
return user;
}
@Bean
public Cat cat(){
Cat cat = new Cat();
cat.setName("大橘");
cat.setColor("橘色");
return cat;
}
}
User user = context.getBean("user", User.class);
Cat cat = context.getBean("cat", Cat.class);
System.out.println(user.getCat() == cat);
结果为:true
我们将@Configuration(proxyBeanMethods = false),再次进行测试。
结果为:false
总结:
proxyBeanMethods:代理bean的方法
-
Full(proxyBeanMethods = true)、【保证每个@Bean方法被调用多少次返回的组件都是单实例的】
-
Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】
-
组件依赖必须使用Full模式默认。其他默认是否Lite模式
1.6.1.2 @Import注解
首先我们来件@Import源码:
@Import注解源码的定义非常简单,就一个属性 value,而且是一个 Class类型的数组。
结合上面的注释,对我们了解Import有很大的帮助。
- 可以同时导入多个 @Configuration类 、ImportSelector 和 ImportBeanDefinitionRegistrar 的实现,以及导入普通类(4.2版本开始支持)
- @Import的功能与 xml 中的
<import/>
标签等效 - 在类级别声明或作为元注释
- 如果需要导入XML 或其他非bean 定义资源,请使用@ImportResource注解
1.6.1.2.1 导入普通类
- 定义一个类
public class Animal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
'}';
}
}
- 使用@Import注解导入这个类
@Import(value = {Animal.class})
public class MyConfig {
}
- 测试
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
//获取所有bean的名称的集合
String[] beanDefinitionNames = context.getBeanDefinitionNames();
//循环获取bean的名称
for(String name : beanDefinitionNames){
System.out.println(name);
}
}
我们打开控制台发现:
我们发现,使用@Import导入的bean的名称默认是类的全限定名!!!
1.6.1.2.2 导入配置类
- 定义一个配置类
@Configuration
public class Student {
}
- 导入配置类
@Configuration(proxyBeanMethods = true)
@Import(value = {Animal.class, Student.class})
public class MyConfig {
}
- 测试
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
//获取所有bean的名称的集合
String[] beanDefinitionNames = context.getBeanDefinitionNames();
//循环获取bean的名称
for(String name : beanDefinitionNames){
System.out.println(name);
}
Student student = context.getBean(Student.class);
System.out.println(student);
}
}
这个bean的名称默认就是类名小写。这个bean的类型呢?
com.xq.pojo.Student$$EnhancerBySpringCGLIB$$231b5261@5f574cc2
我们发现 被 @Configuration
标注的类会被 CGLIB 进行代理。
1.6.2.2.3 ImportSelector接口实现的类被导入
- 定义一个需要被导入的bean
public class ObjectA {
public void objectA(){
System.out.println("objectA");
}
}
- 创建 一个类,实现
ImportSelector
接口,并重写selectImports
方法,方法返回的是需要导入类的全限定名的数组。
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 法返回的是需要导入类的全限定名的数组。
return new String[]{ObjectA.class.getName()};
}
}
- 导入目标类
@Import(value = {MyImportSelector.class})
public class MyConfig {}
- 测试
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
ObjectA obj = context.getBean(ObjectA.class);
System.out.println(obj);
}
}
被spring容器管理的那个bean
com.xq.pojo.ObjectA@55a8dc49
1.6.2.2.4 实现ImportBeanDefinitionRegistrar接口
- 创建一个需要导入的bean
public class ObjectB {
public void objectB(){
System.out.println("objectB");
}
}
- 创建一个类,实现ImportBeanDefinitionRegistrar 接口
public class ImportConfig implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// containsBeanDefinition:判断容器中是否存在指定的 bean 定义,true 存在
boolean flag = registry.containsBeanDefinition(ObjectB.class.getName());
if (!flag){
RootBeanDefinition beanDefinition = new RootBeanDefinition(ObjectB.class);
//注册BeanDefinition,并指定 beanName
registry.registerBeanDefinition("objectB", beanDefinition);
}
}
}
- 导入类
@Import(value = {ImportConfig.class})
public class MyConfig {
}
- 测试
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
ObjectB obj = context.getBean(ObjectB.class);
System.out.println(obj);
}
}
被spring容器管理的那个bean
com.xq.pojo.ObjectB@38d5b107
1.6.1.3 @Conditional注解
当我们构建一个 Spring 应用的时候,有时我们想在满足指定条件的时候才将某个 bean 加载到应用上下文中, Spring 4提供了一个更通用的基于条件的Bean的创建方式,即使用@Conditional注解,我们可以通过 @Conditional 注解来实现这类操作。
Spring Boot 在 @Conditional 注解的基础上进行了细化,无需出示复杂的介绍信 (实现 Condition 接口),只需要手持预定义好的 @ConditionalOnXxxx 注解印章的门票,如果验证通过,就会走进 Application Context 大厅(注入到ioc容器)。
我们来看:
逐个打开这 13 个注解,我们发现这些注解上有相同的元注解。
从这些标记上我们可以了解如下内容:
- 都可以应用在 TYPE 上,也就是说,Spring 自动扫描的一切类 (@Configuration, @Component, @Service, @Repository, or @Controller) 都可以通过添加相应的 @ConditionalOnXxxx 来判断是否加载
- 都可以应用在 METHOD 上,所以有 @Bean 标记的方法也可以应用这些注解
- 都是用了 @Conditional 注解来标记,OnBeanCondition 等自定义 Condition 还是实现了 Condition 接口的,换汤不换药,没什么神秘的,只不过做了更具象的封装罢了。
在这里简单介绍这些注解的作用。
@ConditionalOnBean
仅仅在当前上下文中存在某个对象时,才会实例化一个Bean
@ConditionalOnClass
某个class位于类路径上,才会实例化一个Bean
@ConditionalOnExpression
当表达式为true的时候,才会实例化一个Bean
@ConditionalOnMissingBean
仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean
@ConditionalOnMissingClass
某个class类路径上不存在的时候,才会实例化一个Bean
@ConditionalOnNotWebApplication
不是web应用
@ConditionalOnProperty
指在配置里配置的属性是否为true,才会实例化一个Bean)
@ConditionalOnResource
如果我们要加载的 bean 依赖指定资源是否存在于 classpath 中,那么我们就可以使用这个注解
@ConditionalOnJndi
只有指定的资源通过 JNDI 加载后才加载 bean
@ConditionalOnCloudPlatform
只有运行在指定的云平台上才加载指定的 bean,CloudPlatform 是 org.springframework.boot.cloud 下一个 enum 类型的类
下面我们就以ConditionalMissingBean来简单使用一下。
- 定义配置类
@Configuration
@ConditionalOnBean(name = "cat") //只有springioc容器中存在名为cat这个bean的时候,才会在ioc中将这个类里面的所有@Bean注解修饰的组件注入到ioc容器
public class MyConfig {
@Bean
public User user(){
User user = new User();
user.setUsername("eric");
user.setAge(18);
//cat这个bean在spring容器不存在,因为@Bean注解被注释了
user.setCat(cat());
return user;
}
//@Bean
public Cat cat(){
Cat cat = new Cat();
cat.setName("大橘");
cat.setColor("橘色");
return cat;
}
}
- 测试
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
User user = context.getBean(User.class);
System.out.println(user);
}
}
此时报了找不到user这个bean的异常。说明User这个bean并没有注入到ioc容器中。
我们再换@ConditionalOnMissingBean(name = “cat”)这个注解。此时bean又可以注入到ioc容器中。说到底也是按照条件进行装配。
1.6.2 原生配置文件引入的注解
springboot默认是不识别xml配置文件的,如果我们实在想在springboot项目中使用xml配置文件,需要使用@ImportResource
注解导入xml配置文件。
需求:比如现在我们想在容器中注入UserService。
- 定义UserService
public interface UserService {
public void addUser();
}
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("新增用户");
}
}
- 定义XML配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="com.xq.service.impl.UserServiceImpl"></bean>
</beans>
-
导入xml配置文件
在启动类上导入:
@SpringBootApplication
@ImportResource("classpath:bean.xml") //导入XML配置文件
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
User user = context.getBean(User.class);
System.out.println(user);
}
}
- 测试
http://localhost:8888/user/add
1.6.3 配置绑定注解
在springboot如何完成属性的注入。在原生的spring中我们可以使用xml的方式进行注入。在springboot中又如何做呢?这时候可以使用注解ConfigurationProperties
。
需求:给组件Car完成属性的注入
第一种:
- 定义car类
public class Car {
private String name;
private Integer price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
-
在配置文件定义car的属性
在application.properties里面定义:
car.name=BMW530LI
car.price=550000
- 在Car类上添加属性注入的注解
@Component
@ConfigurationProperties(prefix = "car")//prefix:前缀
public class Car {
//省略
}
- 测试
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
Car car;
@RequestMapping("car")
public Car car(){
return car;
}
}
第二种:
使用@ConfigurationProperties
+ @EnableConfigurationProperties
注解组合使用。
- 在Car上面定义
@ConfigurationProperties
@ConfigurationProperties(prefix = "car")
public class Car {
//属性省略定义
}
- 在配置类上面使用
@EnableConfigurationProperties
注解
@Configuration
@EnableConfigurationProperties(Car.class)//开启Car的属性配置功能,将Car导入到ioc容器中
public class MyConfig {}
1.7 springboot自动配置原理入门
1.7.1 springboot源码分析–自动包规则原理
我们之前提到:springboot默认包扫描的规则是启动类所在包下面及其子包下面。那么在源码中是如何定义的呢?这个时候,我们可以去看看springboot源码是如何定义的。
首先我们找到启动类的核心注解@SpringBootApplication
。
点击进入这个核心注解:
首先是@SpringBootConfiguration
注解,这个注解点击进去,我们发现,它其实描述的就是当前注解修饰的类是配置类:
也就是说我们springboot启动类就是一个配置类
我们再看@ComponentScan
,这个注解我们在spring课程学习过,就是开启自动包扫描,所以在这里也不做详解。
最后我们看看@EnableAutoConfiguration
注解。我们点击进去看看
我们发现@EnableAutoConfiguration
注解又包含两个字注解:分别是@AutoConfiguratonPackage
注解和@Import
注解。
我们先看@AutoConfiguratonPackage
注解。我们点击进去:
继续点击进入这个类:
这个方法就是利用Registrar给容器中导入一系列组件,那到底怎么注册的呢,我们可以打一个断点:
通过打断点发现:它就是把这个注解所在包下面的所有组件导入到容器中。
那包名到底是什么呢?我们可以计算一下:
我们发现:
这就解释了我们之前在搭建springboot工程的时候,要求所有组件必须定义在启动类包下面了,只有这样才能被扫描的到!!!
1.7.2 springboot源码分析–初始加载配置类
@SpringBootApplication
注解,这个注解点击进去,我们发现@EnableAutoConfiguration
注解,我们上一节讨论了@AutoConfigurationPackage
注解。接下来我们讨论@Import({AutoConfigurationImportSelector.class})
注解。
我们点击进入AutoConfigurationImportSelector这个类。里面有一个方法selectImports就是给给容器中批量导入一些组件的方法。
selectImports方法内部的getAutoConfigurationEntry方法才是真正执行导入组件的操作。
getAutoConfigurationEntry方法里面的getCandidateConfigurations方法导入候选配置信息。我们可以打断点来看看,到底加载了哪些候选配置信息。
问题来了,这124项候选配置信息到底从何而来?
我们继续进入getCandidateConfigurations方法内部。
我们进入loadFactoryNames方法内部:
继续进入loadSpringFactories方法内部:
到现在我们明白了,这124项候选配置信息的加载是从spring.facories文件中加载 的。那么这个spring.factories文件在哪里???
我们打开spring.factories文件:
…
从22开始,一直到145行,我们发现不多不少,刚刚是124(145-22+1)项配置信息。
springboot把这124项候选配置信息全部导入到容器里了吗?其实并不是。接下来我们分析springboot的自动配置流程。
1.7.3 springboot源码分析–自动配置流程
springboot批量导入的配置信息并没有全部生效,接下来我们来验证一下。
1.7.3.1 AOP相关的配置类
在加载候选配置信息的时候,也是加载了的。
我们打开AopAutoConfiguration配置类。我们发现,AopAutoConfiguration配置类需要生效,需要满足很多条件。
所以这个配置类是在ioc容器里面生效了的。我们可以验证一下:
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
//根据bean的类型,获取容器中bean的名称
String[] beanNamesForType = context.getBeanNamesForType(AopAutoConfiguration.class);
System.out.println(beanNamesForType.length);// 值为1 说明这个bean已经在ioc容器中存在。
}
}
我们进入这个配置类继续观察,我们发现这个配置类的内部也在注入一些组件:
但是我们搜索发现,容器中并不存在org.aspectj.weaver.Advice这个bean!!!说明AspectJAutoProxyingConfiguration这个bean并没有注入到容器中。
1.7.3.2 验证缓存相关的配置类
我们先来看看缓存相关的自动配置。
CacheAutoConfiguration有没有导入到IOC容器,其实在加载候选配置信息的时候,已经导入了的。
我们打开这个配置类:
我们发现,CacheAutoConfiguration配置类需要生效,同样需要满足很多条件。
- @Configuration(proxyBeanMethods = false) 标识当前类是一个配置类。
- @ConditionalOnClass({CacheManager.class}) 条件注入,意思是当前配置类生效,类路径下必须存在CacheManager这个类,容器中存在吗?我们可以搜索一下:
- @ConditionalOnBean({CacheAspectSupport.class}) 条件注入,意思是当前配置类生效,容器中必须存在CacheAspectSupport这个bean。我们可以测试一下容器中是否存在这个bean。
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
//根据bean的类型,获取容器中bean的名称
String[] beanNamesForType = context.getBeanNamesForType(CacheAspectSupport.class);
System.out.println(beanNamesForType.length); // 0
}
}
我们发现容器中并没有这个bean的名称,说明这个bean在容器中压根没有。
其实看到这里,我们就不用往下看了,因为CacheAspectSupport这个bean在ioc容器中不存在,所以当前的配置类CacheAutoConfiguration也不会生效。
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
//根据bean的类型,获取容器中bean的名称
String[] beanNamesForType = context.getBeanNamesForType(CacheAutoConfiguration.class);
System.out.println(beanNamesForType.length); //为0,说明没有在ioc容器中生效
}
}
1.7.3.3 验证springmvc相关的配置类
(1) 我们先来看看DispatcherServletAutoConfiguration配置类。
这个配置其实是生效了的。我们可以验证一下:
- @Configuration(proxyBeanMethods = false) 标识当前类是一个配置类
- @ConditionalOnWebApplication(type = Type.SERVLET) 当前配置类生效的前提是当前工程是web工程(我们创建的springboot工程是一个web工程,因为我们引入了web启动器)。
- @ConditionalOnClass({DispatcherServlet.class}) 当前配置类生效的前提是DispatcherServlet这个类必须存在。我们验证发现,这个类确实存在:
- @AutoConfigureAfter({ServletWebServerFactoryAutoConfiguration.class}) 当前配置类生效的前提是ServletWebServerFactoryAutoConfiguration这个配置类生效。我们验证一下:
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
//根据bean的类型,获取容器中bean的名称
String[] beanNamesForType = context.getBeanNamesForType(ServletWebServerFactoryAutoConfiguration.class);
System.out.println(beanNamesForType.length); //值为1 说明ioc容器中存在这个配置类的bean
}
}
综上所述:DispatcherServletAutoConfiguration这个配置类在ioc容器中生效了。
@SpringBootApplication
public class App {
public static void main(String[] args) {
//获得ioc容器
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
//根据bean的类型,获取容器中bean的名称
String[] beanNamesForType = context.getBeanNamesForType(DispatcherServletAutoConfiguration.class);
System.out.println(beanNamesForType.length);// 值为1 说明这个bean在ioc容器中存在。
}
}
我们继续看DispatcherServletAutoConfiguration这个配置类的内部:
这几个组件其实都注入到了ioc容器中,大家可以自行测试。
在这里我们重点关注MultipartResolver这个bean的注入过程。
@Bean
@ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名multipartResolver 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
//给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
//SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
(2) HttpEncodingAutoConfiguration配置类。
为什么我们在springboot项目中响应中文信息给前端不会乱码,其实就是springboot已经给我们做了编码过滤器相关的自动配置,我们来看一下:
@RestController
public class HelloController {
@RequestMapping("hello")
public String hello(){
return "大家好"; //响应的是中文
}
}
我们先来看看这个配置类:
接下来我们再看这个配置类的内部信息:
注意:SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先
我们先来看看CharacterEncodingFilter这个组件的注入过程:
我们点击进入ServerProperties中:
我们可以在配置文件中尝试配置:
如果我们想修改springboot默认配置的信息,我们只需要通过修改配置文件即可生效。
总结:
-
SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
-
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定
-
生效的配置类就会给容器中装配很多组件
-
只要容器中有这些组件,相当于这些功能就有了
-
定制化配置
-
- 用户直接自己@Bean替换底层的组件
- 用户去看这个组件是获取的配置文件什么值就去修改。
1.7.4 springboot的最佳实践
-
引入场景依赖
- https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters
-
查看自动配置了哪些(选做)
- 自己分析,引入场景对应的自动配置一般都生效了
- 配置文件中debug=true开启自动配置报告。Negative(不生效)\Positive(生效)
-
是否需要修改
-
参照文档修改配置项
- https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties
- 自己分析。xxxxProperties绑定了配置文件的哪些。
-
比如我们想修改springboot启动图标,我们可以参考官方提供的配置信息
spring.banner.image.location=classpath:abc.jpg
-
自定义加入或者替换组件
- @Bean、@Component。。。
-
自定义器 XXXXXCustomizer;
1.8 springboot开发小技巧
1.8.1 lombok简化开发
lombok是一个插件,用途是使用注解给你类里面的字段,自动的加上属性,构造器,ToString方法,Equals方法等等,比较方便的一点是,你在更改字段的时候,lombok会立即发生改变以保持和你代码的一致性。
- 安装lombok插件
点击settings
点击Browse repositories
注意:安装完后一定要重启idea。
- 引入lombok依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
由于springboot已经对lombok的版本进行了管理,所以我们不需要定义lombok的版本。
-
使用lombok
- 简化javaBean开发
@Data //省略定义get set toString方法 @NoArgsConstructor //不用写无参数的构造函数 @AllArgsConstructor //不用定义带所有参数的构造函数 public class User { private String username; private Integer age; private Cat cat; }
- 简化日志开发
@RestController @Slf4j public class HelloController { @RequestMapping("hello") public String hello(){ log.info("hello方法执行了......"); return "大家好"; } }
执行控制器方法,观察控制台:
1.8.2 dev-tools热部署工具
在实际项目开发中,开发的过程中一定会修改代码,如果每次修改代码都需要重新启动下,那会让人吐血的。这里我们使用Spring-boot-devtools进行热部署。
- 引入热部署依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
- 引入插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
- 修改IDEA配置
- shift + ctrl + alt + / 四个按键一块按,选择Reg项
- 点击进入Registry
1.8.3 Spring Initailizr
Spring Initializr 从本质上说就是一个Web应用程序,它能为你构建Spring Boot项目结构。虽然不能生成应用程序代码,但它能为你提供一个基本的项目结构,以及一个用于构件代码的Maven或者Gradle构建说明文件。
点击Next进入下一步:
点击Next进入下一步:
选择需要引入的依赖
点击下一步完成,我们发现springboot会自动帮助我们引入启动器,并创建好工程目录和启动器。
依赖也引入进来了:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2 SpringBoot2核心技术–核心功能
SpringBoot使用一个全局的配置文件,配置文件名是固定的;
-
application.properties
-
application.yml
现在我们重点讨论yml配置文件的语法。
2.1 YML文件的语法
-
k:(空格)v:表示一对键值对(空格必须有);
-
以缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级的;
-
属性和值也是大小写敏感;
-
#表示注释;
-
字符串无需加引号,如果要加,’ '与" "表示字符串内容 会被 转义/不转义
server:
port: 8088 #修改项目端口号
2.1.2 YML文件的值的写法
2.1.2.1 字面量:普通的值(字符串,数字,布尔值)
k: v:字面直接来写;
字符串默认不用加上单引号或者双引号;
“”:双引号;会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思
name: “zhangsan \n lisi”:输出;zhangsan 换行 lisi
‘’:单引号;不会转义特殊字符,特殊字符终只是一个普通的字符串数据
name: ‘zhangsan \n lisi’:输出;zhangsan \n lisi
username: eric # 不用带双引号
name1: 'zhangsan \n lisi' # 将/n转义成换行
name2: "zhangsan \n lisi" # 不会转义
我们可以测试一下,将配置文件的这些属性绑定在一个类的属性上。
(1) 定义一个类
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Demo1 {
@Value("${username}")
private String username;
@Value("${name1}")
private String name1;
@Value("${name2}")
private String name2;
}
(2)定义controller
@RestController
public class HelloController {
@Autowired
Demo1 demo1;
@RequestMapping("demo1")
public String demo1(){
System.out.println(demo1);
return "hello";
}
}
(3) 测试
执行请求: http://localhost:8088/demo1,观察控制台效果。
2.1.2.2 复杂类型数据的写法
- 对象的写法(map也是一样)
k: v:在下一行来写对象的属性和值的关系;注意缩进
animal: # 定义对象
name: Sunny
age: 12
friend: {fname: Oscar,age: 20} # 对象的行内写法
- 数组 list set的写法
pets:
- cat
- dog
- fish
names: [cat,dog,fish] # 行内写法
2.1.2.3 复杂类型数据的注入
- 定义配置文件
person:
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,weight: 47}
health: [{name: mario,weight: 47}]
- 定义java类
@Component
@Data
@ConfigurationProperties(prefix = "person")
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
@Component
@Data
@ConfigurationProperties(prefix = "person.pet")
public class Pet {
private String name;
private Double weight;
}
2.1.3 配置文件自动提示的问题
现在我们发现有一个问题,就是我们自己写的配置信息没有提示,但是spring官方的配置信息是有提示信息的。我们只需要引入一个依赖就可以解决问题。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
我们测试:
2.2 springboot web开发
在springboot项目中,我们发现,我们无需任何配置就可以使用springmvc,那是因为springboot帮助我们进行了springmvc的自动配置。那么springboot到底配置了springmvc的哪些功能呢?我们可以查阅官方文档。
点击进入web:
我们大致翻译一下就可以看出来:
-
Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.- 内容协商视图解析器和BeanName视图解析器
-
Support for serving static resources, including support for WebJars (covered later in this document)).
- 静态资源(包括webjars)
-
Automatic registration of
Converter
,GenericConverter
, andFormatter
beans.- 自动注册
Converter,GenericConverter,Formatter
- 自动注册
-
Support for
HttpMessageConverters
(covered later in this document).- 支持
HttpMessageConverters
(后来我们配合内容协商理解原理)
- 支持
-
Automatic registration of
MessageCodesResolver
(covered later in this document).- 自动注册
MessageCodesResolver
(国际化用)
- 自动注册
-
Static
index.html
support.- 静态index.html 页支持
-
Custom
Favicon
support (covered later in this document).- 自定义
Favicon
- 自定义
-
Automatic use of a
ConfigurableWebBindingInitializer
bean (covered later in this document).- 自动使用
ConfigurableWebBindingInitializer
,(DataBinder负责将请求数据绑定到JavaBean上)
- 自动使用
当然我们也可以定制化springmvc相关的功能,在spingboot官方文档,也有对应的描述:If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own
@Configuration
class of typeWebMvcConfigurer
but without@EnableWebMvc
.
不用@EnableWebMvc注解。使用 **@Configuration**
+ **WebMvcConfigurer**
自定义规则
If you want to provide custom instances of
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of typeWebMvcRegistrations
and use it to provide custom instances of those components.
声明 **WebMvcRegistrations**
改变默认底层组件
If you want to take complete control of Spring MVC, you can add your own
@Configuration
annotated with@EnableWebMvc
, or alternatively add your own@Configuration
-annotatedDelegatingWebMvcConfiguration
as described in the Javadoc of@EnableWebMvc
.
使用 **@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC**
2.2.1 web开发–静态资源规则与定制化
springboot对静态资源的访问提供了固定的映射规则,我们只需要根据官方的映射规则定义静态资源就可以正常访问,我们可以看看官网的描述:
- 根据官方文档的描述,我们发现只要我们把静态资源放在类路径下的static或public或resources或/META-INF/resources目录下面,我们就可以访问,现在我们去试一下。
我们创建对应的目录,分别在目录里面定义html静态资源去访问。
我们重启项目,根据访问:当前项目根路径/ + 静态资源名的规则访问:
http://localhost:8088/a.html
http://localhost:8088/b.html
http://localhost:8088/c.html
http://localhost:8088/d.html 经过测试都能访问。
现在我们思考一个问题:
如果我定义一个动态资源,同时也存在一个同名映射路径的静态资源,那么最终会访问动态资源还是静态资源?
比如我们定义一个映射路径是a.html的动态资源。
@RequestMapping("a.html")
public String hello(){
return "hello";
}
重启项目访问:
http://localhost:8088/a.html
总结:
当请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。
- 如果存在同名的静态资源和动态资源,我们肯定都要能访问。这个时候,我们可以给静态资源配合一个请求前缀。这样就可以解决静态资源不能被访问的问题。如何配置,在官方文档上也给我们做了描述:
我们在配置文件里面按照官方文档的描述进行修改:
spring:
mvc:
static-path-pattern: /hello/** #配置访问静态资源的前缀
比如我们现在想访问c.html。
我们先用之前的访问规则:
我们再加上前缀来访问:
http://localhost:8088/hello/c.html
- 同样的我们也可以也可以自定义静态文件的默认映射规则,这一点在官方文档上也有描述:
spring:
resources:
static-locations: [classpath:/hello/] #改变静态资源的默认映射规则
我们测试一下:
- springboot也支持对Webjars资源的访问。
Webjars也就是把静态资源打成jar包让我们访问。
需求:我们想在springboot项目里面访问jquery库文件。\
第一步: 引入jquery相关的依赖。
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
我们在项目中可以看到jquery文件已经引入。
第二步:重启项目,访问jquery资源
http://localhost:8088/webjars/jquery/3.5.1/jquery.js
2.2.2 web开发–welcome与favicon功能
- 配置欢迎页
springboot支持将index.html放置在静态文件映射的目录,我们就可以直接访问index.html.
我们重启项目:
localhost:8088
- 我们也可以给我们的请求配置图标favicon
我们只需要把favicon.ico放置在静态资源的目录即可
放置好了之后,重启项目:
注意:如果我们配置了静态资源的映射前缀,那么首页index.html是访问不了的。
spring:
mvc:
static-path-pattern: /hello/** #会导致首页index.html访问不了
至于为什么,我们在后面分析源码再来看。
2.2.3 web开发–静态资源映射规则源码解析
静态资源映射规则的自动配置都在WebMvcAutoConfiguration这个配置类里面描述了。我们可以打开进行源码追踪。
通过分析源码,我们发现WebMvcAutoConfiguration这个配置类肯定被ioc容器进行了管理(也就是自动配置生效了)。那么它到底给容器里配置了什么呢?
我们先来看WebMvcAutoConfigurationAdapter。
我们发现,WebMvcAutoConfigurationAdapter要生效的话,前提是配置文件的信息要绑定到webMvcProperies和ResourceProperties这两个类上。
我们分别打开WebMvcProperties和ResourceProperties这两个类:
WebMvcProperties:
ResourceProperties:
我们终于知道:WebMvcProperties和spring.mvc开头的配置信息绑定;ResourceProperties和spring.resources开头的配置信息绑定。
注意:WebMvcAutoConfigurationAdapter这个类有一个条件构造器。那么有参构造器所有参数的值(组件)都会从容器中确定。 — 具体信息没有就配默认值
- ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
- WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
- ListableBeanFactory beanFactory Spring的beanFactory
- HttpMessageConverters 找到所有的HttpMessageConverters
- ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。
- ServletRegistrationBean 给应用注册Servlet、Filter…
2.2.3.1 静态资源处理的默认规则
当WebMvcAutoConfigurationAdapter这个组件自动配置生效之后(前提是配置文件的信息要绑定到webMvcProperies和ResourceProperties这两个类上。),我们看看里面的一个**方法addResourceHandlers。**那么这个方法就是在描述静态资源处理的默认规则-----记住哈 。
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
}
我们打断点来跟踪源码:
那this.resourceProperties.isAddMappings()到底是true还是false?
我们可以计算一下:
我们发现这个表达式值为true。
这个表达式到底是什么意思???
我们发现isAddMappings方法是从ReourcesProperties类中获取值,因为在上面的构造函数中,this.resourceProperties的值是从容器中的ResourceProperites组件中拿的。 — 具体的如果没有就给默认值啦
我们最终明白isAddMappings的值是被配置文件的属性影响的。
spring:
resources:
add-mappings: true #默认值
如果把这个值变成false,那么我们所有静态资源映射规则就不会生效。
我们可以通过源码追踪给出答案:
所以spring.resources.add-mappings的值千万不能为false。否则静态资源不生效。
- webjars请求资源的映射规则
spring:
resources:
add-mappings: true
cache:
period: 11000 #配置静态资源在浏览器的缓存时间
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
这就证明了我们之前访问jquery资源的映射路径为什么要这么写:
http://localhost:8088/webjars/jquery/3.5.1/jquery.js
/**请求资源的映射规则
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
我们打断点:
spring:
mvc:
static-path-pattern: /** #默认值
/**请求映射的静态资源的目录我们也可以计算出来:
2.2.3.2 欢迎页的处理规则
我们发现WelcomePageHandlerMapping这个类封装了具体的欢迎页访问规则。我们点击进去看看。
2.2.4 web开发–rest请求处理原理
和springmvc一样,我们可以通过@RequestMapping注解接收用户的请求。
@RestController
public class RequestController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-请求";
}
}
springboot也支持Rest风格请求处理。
-
- 以前: /getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
- 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
我现在做一个测试:
- 定义HTML
<!--get请求-->
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit"/>
</form>
<!--post请求-->
<form action="/user" method="post">
<input value="REST-POST 提交" type="submit"/>
</form>
<!--delete请求-->
<form action="/user" method="post">
<input value="REST-DELETE 提交" type="submit"/>
</form>
<!--put请求-->
<form action="/user" method="post">
<input value="REST-PUT 提交" type="submit"/>
</form>
- 定义后台controller
@RestController
public class RequestController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-请求";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-请求";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-请求";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-请求";
}
}
此时我们发生请求,发现一个问题。
当执行delete和put请求的时候,我们发现请求并没有进入到对应的后台控制器方法处理。
为了解决这个疑问,我们需要了解springboot对后台请求处理的原理。
只要是表单提交的方法都是交给这个过滤器(HiddenHttpMethodFilter(继承了它))来处理的。通过红色框里的内容描述的很清楚,只有配置spring.mvc.hiddenmethod.filter.enabled这个过滤器才会起作用。当然如果我们不配,这个值的默认值为false。
但是过滤器为什么没有帮助我们处理delete和put的请求呢。就是这个值为false的原因。现在我们把他更改为true。
spring:
mvc:
static-path-pattern: /**
hiddenmethod:
filter:
enabled: true
我们发现OrderedHiddenHttpMethodFilter 继承了HiddenHttpMethodFilter这个类。也就是说具体过滤的逻辑在HiddenHttpMethodFilter里面。我们打开HiddenHttpMethodFilter这个类。
我们现在发送delete或者put请求,发现还是没有起作用。这个时候我们打断点,来看看请求处理的具体细节
当请求进入doFilterInternal方法之后,springboot把我们的请求包装成了一个HttpServletRequest对象。当进行if判断的时候,我们发现request.getMethod方法的值为POST。也就是虽然我们表单请求的是DELETE,但是底层给我们转换成了POST了(因为 delete上, 写的是post)。(这也是为什么我们的delete put请求最后被@RequestMapping(value = “/user”,method = RequestMethod.POST)修饰的方法处理了的原因----后话)。
我们计算表达式得出的结果:
进入if语句之后,String paramValue = request.getParameter(this.methodParam);的值为null。此时我们的请求就直接交给filterChain.doFilter((ServletRequest)requestToUse, response);处理了。
为什么String paramValue = request.getParameter(this.methodParam);的值为null。是因为我们在表单里面没有设置这个值,自然也就获取不到。什么样的值呢???
所以我们需要在表单内部定义这个参数及其对应的值。 _method
value值就是不同的请求方式,取名get post delete put 不区分大小写。
我们再重启项目,打断点测试:
我们发送delete请求:
这样我们的delete请求或者put请求就能够被处理了。
注意:这种rest映射处理逻辑只能针对于表单提交的请求。
到这里,我们就清楚了springboot对表单发送的请求处理流程了。但是我们可以使用更简单的注解对后台控制器方法进行优化。
<!--get请求-->
<form action="/user" method="get">
<input name="_method" type="hidden" value="get"/>
<input value="REST-GET 提交" type="submit"/>
</form>
<!--post请求-->
<form action="/user" method="post">
<input name="_method" type="hidden" value="post"/>
<input value="REST-POST 提交" type="submit"/>
</form>
<!--delete请求-->
<form action="/user" method="post">
<input name="_method" type="hidden" value="delete"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>
<!--put请求-->
<form action="/user" method="post">
<input name="_method" type="hidden" value="put"/>
<input value="REST-PUT 提交" type="submit"/>
</form>
@RestController
public class RequestController {
//@RequestMapping(value = "/user",method = RequestMethod.GET)
@GetMapping("/user")
public String getUser(){
return "GET-请求";
}
//@RequestMapping(value = "/user",method = RequestMethod.POST)
@PostMapping("/user")
public String saveUser(){
return "POST-请求";
}
//@RequestMapping(value = "/user",method = RequestMethod.PUT)
@PutMapping("/user")
public String putUser(){
return "PUT-请求";
}
//@RequestMapping(value = "/user",method = RequestMethod.DELETE)
@DeleteMapping("/user")
public String deleteUser(){
return "DELETE-请求" +
"";
}
}
在表单里面我们定义了_method参数。当然我们也可以自定义参数。
我们通过上面的源码发现,我们的表单请求都是交给HiddenHttpMethodFilter进行处理的。我们自己可以重新设置HiddenHttpMethodFilter过滤器的相关属性:
@Configuration
public class HiddenHttpMethodConfig {
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}
2.2.5 web开发–处理器映射器工作原理
我们以发送get请求为例:
当我们发送一个controller请求,后台对应的控制器方法就可以执行,那么是如何找到这个控制器方法的呢?接下来我们就分析springboot中控制器方法映射原理。
在springmvc中,用户发送请求首先会被前端控制器DispatcherServlet拦截。在springboot中同样也有前端控制器。前端控制器会如何处理我们的请求?我们学过springmvc知道,前端控制器DispatcherServlet不会亲自处理用户的请求,在springboot中,请求会被DispatcherServlet的父类FrameworkServlet(框架Servlet)去处理。
FrameworkServlet类中哪些方法可以处理用户的请求?
我们得出,根据用户发现不同类型的请求,交给不同的方法来处理。
我们发现不管哪种请求方式,用户的请求都交给了processRequest(过程请求)方法来处理。所以我们看看这个方法的具体处理逻辑。
processRequest-----》this.deservice-------》this.doDispatch----》this.gethandle —》接下来就是循环获取合适的处理器了 (就容易了 )
我们关注这个方法的重点代码:
我们观察发现用户的请求进一步的交给了DispatcherServlet类里面的doService方法处理。
我们点击进入doService方法内部,关注重点代码:
我们进入doDispatch方法内部:
这里的getHandler方法就是找到当前请求使用哪个Handler(Controller的方法)处理。我们继续点进入这个方法。— 看到了循环方法 (下图是:专门处理控制器方法,被requestMapping修饰的方法 )
这个mapping到底是什么玩意,我们可以看看:
总结就是:所有的请求映射都在HandlerMapping中。
请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。如果有就找到这个请求对应的handler,如果没有就是下一个 HandlerMapping。
我们继续点击进入getHandler方法。
到这里我们就找到了对应的Handler来处理用户的请求了(也可以说把剩下的交给处理器适配器了)。
2.2.6 web开发–springboot常用参数注解使用
在springboot的controller中常用的参数注解有:@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody。下面我们分别对这些注解的使用进行演示。
2.2.6.1 @PathVariable注解
通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的www.xxx.com/user/{xxx}/num/{bbb} 占位符可以通过@PathVariable(“xxx“)。
@RestController
public class ParamController {
@RequestMapping("aa/{user}/mm/{num}")
public String testPathVariable(@PathVariable("user") String userID, @PathVariable("num") String num){
System.out.println(userID);
System.out.println(num);
return "hello";
}
}
发送请求:http://localhost:8088/aa/789/mm/333
控制台输出:
789
333
2.2.6.2 @RequestParam注解
用户在前端拼接参数例如:key=value1&key2=value2参数列表。通过注解@RequestParam可以轻松的将URL中的参数绑定到处理函数方法的变量中。
@RequestMapping("aa")
public String testRequestParam(@RequestParam String uu,
@RequestParam("user") String userID,
@RequestParam("num") String num){
return uu + "--" + userID + "--" + num;
}
http://localhost:8088/aa?uu=22&user=44&num=55
有的时候匹配的参数没有,则需要进一步处理http://localhost:8088/aa?uu=22&user=44。缺少了参数num。如果不进行处理,则会后台报错。
@RequestParam(name="num",required=false,defaultValue="0")
2.2.6.3 @RequestBody注解
在同步的状态下,接收前台表单提交的实体内容。
定义表单:
<!--提交实体内容-->
<form action="getData" method="post">
<input type="text" name="username"> <br>
<input type="password" name="password"><br>
<input type="submit" value="提交"/>
</form>
定义控制器方法
@RequestMapping("getData")
public String getData(@RequestBody String data){
return data;
}
实体内容:
username=admin&password=admin
2.2.6.4 @CookieValue注解
获取指定的cookie信息的值。
@RequestMapping("getCookie")
public String getCookie(@CookieValue("Idea-10501acc") String cookie){
return cookie;
}
2.2.6.5 @RequestHeader注解
获取指定请求头相关的信息
@RequestMapping("getHeader")
public String getHeader(@RequestHeader("Accept-Encoding") String header){
return header;
}
2.2.6.6 @RequestAttribute注解
用来标注在接口的参数上,参数的值来源于 request 作用域。
我们使用test1转发,并在request作用域里面存储值。在test2里面的形式参数里面获取的就是test1方法存储在Request作用域的值。
@Controller
public class ParamController {
@RequestMapping("/demo1/test1")
public String test1(HttpServletRequest request)
request.setAttribute("site", "hello,springboot");
return "forward:/demo1/test2";
}
@RequestMapping(value = "/demo1/test2")
@ResponseBody
public String test2(@RequestAttribute("site") String site) {
return site;
}
}
注意:上面的是@Controller注解,千万不能写成@RestController。
2.2.6.7 @Matrixvariable注解
我们的请求风格有三种方式:
- queryString请求方式(?拼接的方式): /request?username=admin&password=123456&age=20
- rest风格请求:/request/admin/123456/20
- 矩阵变量请求风格:/request/path;username=admin;password=123456;age=20,21,22
矩阵变量请求是一种新的请求风格,严格来说矩阵变量的请求需要用到rest风格但是又不同于rest。
在springboot中,矩阵变量的使用默认是不开启的。如果使用需要手动开启。
@Configuration
public class HiddenHttpMethodConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
//开启矩阵变量的使用权限
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
(1)编写html
<a href="learn/metrix/color=red,blue,green">矩阵变量</a>
(2)编写后台controller
@RequestMapping("/learn/metrix/{param}")
public String LearnMetrix(@MatrixVariable(pathVar="param",value="color")String[] yanse){
System.out.println("得到的参数有:");
for (String s : yanse) {
System.out.println(s);
}
return "welcome";
}
(3)测试
2.2.7 web开发–参数解析原理
在上面的章节,我们讲到了springboot常用注解,我们发现,只要给控制器方法的参数加上相应的注解,那么springboot就会自动把参数值给我们获取到,那么springboot底层到底是如何帮助我们获取参数的值呢?接下来我们来看一看底层原理是怎么实现的。
我们首先打开DispatcherServlet这个类。我们知道任何请求都会被前端控制器拦截。前端控制器里面有一个doDispatch方法来处理我们的请求。这个方法里面有一个getHandler方法如下:(处理映射器工资原理 上面讲过了)
这个方法是通过处理器映射器帮助我们查找指定的Handler(上面章节已经讨论这部分原理)。
那么Handler又交给谁处理呢?通过源码,我们发现前端控制器将Handler交给处理器适配器处理,所以我们首先就要获取处理器适配器。(那就 类似于SpringMVC喽)
---------------也是循环获取最匹配的适配器
我们打断点进入到这个方法内部,发现:-- – 也是通过循环
这个4个处理器适配器就是处理不同的控制器方法。其中第一个@RequestMappingHandlerAdapter适配器最重要。就是用来处理@RequestMapping注解修饰的方法。
通过源码观察到,如何获取最合适的处理器适配器呢,就是循环遍历,获取指定的适配器去处理控制器方法。
当找到适配器之后,通过适配器来进行handler的处理
那么这个handle方法是怎么处理的呢?我们可以点击进去看看。
照这个接口的实现类 — 进一部观察handle。
我们查询AbstractHandlerMethodAdapter类中的handle方法。
接下来就重要了!
我们发现这个handle方法内部调用了一个handleInternal方法。我们进入这个方法的内部处理细节。
我们进入RequestMappingHandlerAdapter类中的handleInternal方法。
这个this.invokeHandlerMethod方法按照字面意思理解就是执行目标方法。我们点击进去看看细节。
它内部有一个 参数解析器(argumentResplvers)
还有一个 返回值处理器 (returnValueHandlers)
这个方法,首先我们关注参数解析器(this.argumentResolvers)。非常重要,我们打断点进去看看。
参数解析器的作用就是确定将要执行的目标方法的每一个参数的值是什么;
我们通过参数解析器的名称可以确定:
RequestParamMethodArgumentResolver专门解析@RequestParam注解修饰的参数值。
PathVariableMethodArgumentResolver 专门解析@PathVariable注解修饰的参数值。
这些参数解析器都实现了一个接口:HandlerMethodArgumentResolver(处理器方法参数解析)。
- supportParametrt:当前解析器是否支持解析这种参数
- 支持解析就调用 resolveArgument方法-----也就是说满足条件 就调用这个解析方法
我们再来看,this.returnValueHandlers。这个是返回值处理器。我们打断点看看:
在这个类中有个invocableMethod.invokeAndHandle方法 。
接下来我们继续打断点,走到invokeAndHandle方法。这个方法字面意思是执行并处理。
我们进入这个方法内部:
经过测试,我们得到了一个非常重要的结论:
**当this.invokeForRequest方法执行之后,立马会执行我们的控制器方法。**也就是这个invokeRequest方法会真正执行我们的目标方法,那目标方法到底是怎么执行的?我们进入这个方法---- 进入this.invokeForRequest方法 。
也就是我们的返回值处理器一放行就可以完整的执行了。
我们发现:getMethodArgumentValues这个方法就是获取我们的参数值,然后将参数值封装到Object[]数组中**。然后调用this.doInvoke方法进行执行。也就是按照反射的机制调用目标方法**。我们查看doInvoke的具体细节,看是不是使用反射的机制实现的。
我们发现通过一个反射工具类进行反射调用。
我们明白this.getMethodArgumentValues方法就是获取目标方法的参数值。接下来我们就进入这个方法,探究目标方法的核心参数值是如何获取的?
我们测试一下:比如我们发送的请求:http://localhost:8088/aa/a/mm/2?username=eric
目标方法是这样的:
我们来看看参数的详细信息:
我们继续往下看,我们发现获取参数的详细信息之后,又声明了一个空的object类型的数组。
我们继续往下看:
我们发现,获取到目标方法的详细参数信息之后,会循环遍历装载参数详细的这个parameters数组。**判断参数解析器是否支持解析这个参数。它是如何判断的?**我们进入this.resolvers.supportsParameter(parameter))。
我们可以跟踪源代码,循环遍历这26个参数解析器。
获取到参数解析器之后,如何判断对应的参数解析器是否支持解析参数呢?我们进入supportsParameter方法内部。
由于我们第一个参数是@PathVariable注解修饰的。所以肯定交给对应的PathVariableMethodArgumentResolver参数解析器来判断。
当我们的参数被参数解析器支持解析之后,接下来我们看看参数具体如何解析的。
我们进入resolvers.resolveArgument方法内部-----------预热结束 --进行真正的解析: 好好记住
根据参数类型 会找到对应的方法 。
我们进入resolveArgument方法内部。进入AbstractNamedValueMethodArgumentResolver类的resolveArgument****方法。
扩展:
AbstractNamedValueMethodArgumentResolver
这个用来处理 key/value 类型的参数,如请求头参数、使用了 @PathVariable
注解的参数以及 Cookie 等。
再扩展下
AbstractNamedValueMethodArgumentResolver(抽象类)实现了HandlerMethodArgumentResolver,用于解析请求方法参数的值,可以解析@PathVariable、@RequestParam、@RequestHeader、@RequestAttribute、@CookieValue、@SessionAttribute、@MatrixVariable。
————————————————
我们进入resolveName方法,看看具体的实现:
我们点进去:
**我们发现目标方法的参数名称和参数值都存放在了一个map里面。map的key就是参数名称,value就是参数值,**我们可以通过key获取参数值。这样我们的参数值就被解析出来了。这是springboot里面解析@PathVariable注解的原理。
那么@RequestParam注解修饰的 参数如何解析?我们也可以看一看核心的代码。
我们先定义一个控制器代码:
@RequestMapping("testRequestParam")
public String testRequestParam(@RequestParam("username") String username){
System.out.println(username);
return "hello";
}
只不过参数解析交给RequestParamMethodArgumentResolver这个类的resolveName方法来处理了。进入核心代码:
我们追踪核心源码**:String[] paramValues = request.getParameterValues(name);其实就是使用原生的servelet的方式进行参数解析。**
扩展:
RequestParamMethodArgumentResolver
这个功能就比较广了。使用了 @RequestParam
注解的参数、文件上传的类型 MultipartFile、或者一些没有使用任何注解的基本类型(Long、Integer)以及 String 等,都使用该参数解析器处理。
到这里我们springboot参数解析的原理就讲解完毕。
2.2.8 web开发–原生servletAPI的支持原理
我们知道,在springboot中我们也支持传递原生的servlet对象。比如:
@RequestMapping("testServletAPI")
public String testServletAPI(HttpServletRequest request){
return "hello";
}
我们以形参的方式直接传递到控制器方法中就可以用了。那么底层到底是如何实现对原生ServletAPI的支持呢?我们来看看源码是如何解析的。
发送请求:http://localhost:8088/testServletAPI。打断点进入debugg模式
进入InvocableHandlerMethod类,找到getMethodArgumentValues方法:
我们进入this.resolvers.supportsParameter(parameter)方法。看看是如何判断的:
点击supportsParameter方法进入HandlerMethodArgumentResolverComposite类:
我们发现parameter参数封装了控制器方法中参数的详细信息。 我们继续进入this.getArgumentResolver方法。
根据我们之前的经验,我们发现,还是遍历所有参数解析器,找到最合适的参数解析器,那么这个最合适的参数解析器是哪个呢?
我们循环遍历,最后发现就是这个:
拿到了参数解析器,我们如何解析参数呢?
我们点击进去:
注意:resolveArgument里面有个webRequest参数,这个参数就是把原生热request对象和response对象封装到了一起。
继续进入resolveArgument方法:
我们点击进入:
我们继续debugg模式进入getNativeRequest方法。
继续debugg模式进入WebUtils.getNativeRequest(this.getRequest(), requiredType)方法。进入ServletRequestAttributes类:
这样我们就能够拿到原生的Request对象供我们使用了。
2.2.9 web开发–Model Map类型的参数
在springboot中,我们可以在控制器方法中定义Model或者Map来进行数据的传递。我们来看下一段代码:
@Controller
public class ModelController {
@RequestMapping("testModelAndMap")
public String testModelAndMap(Map<String,Object> map, Model model){
map.put("username","eric");
model.addAttribute("gender","sex");
return "forward:success";//转发到success控制器方法
}
@RequestMapping("success")
public @ResponseBody String success(HttpServletRequest request){
//从request作用域取值
System.out.println(request.getAttribute("username"));
System.out.println(request.getAttribute("gender"));
return "success";
}
}
结果:
eric
sex
我们发现,我们可以从request作用域里面将值取出来,这是为什么?我们需要讨论springboot底层对Model Map类型参数的支持。
我们打断点进入InvocableHandlerMethod类:
我们step into进入这个方法内部:
我们把断点打在if语句上,放行断点,查看遍历参数解析器,到底是那个参数解析器解析我们的参数。
我们发现MapMethodProcessor支持对Map类型的参数进行解析。
我们看看resolver.supportsParameter(parameter)是怎么解析的:
我们step into进入supportsParameter方法。
我们观测Map类型的参数能被MapMethodProcessor解析之后,我们来看看到底如何解析的?
我们step into进入resloveArgument方法。
这个mavContainer到底是什么玩意?
通过上面的方法中的参数,我们发现mavContainer是ModelAndViewContainer类型。我们进入这个类看看:
我们发现mavContainer.getModel方法返回了一个BindingAwareModelMap对象。
我们查看BindingAwareModelMap。我们发现:
我们得出一个结论:
BindingAwareModelMap对象既是一个Model也是一个Map。
也就是第一个参数Map经过解析,将其封装成了一个BindingAwareModelMap对象放在了args数组中。
接下来我们看看第二个参数Model是如何解析的:
我们进入supportsPatameter方法内部:
如果我们的参数是Model类型,参数解析器就支持解析,很明显我们的参数的Model类型。
当ModelMethodProcessor参数解析器支持对Model参数进行解析之后,我们看看具体参数解析的步骤:
我们step into进入resolveArgument方法内部:
我们继续计入resolveArgument方法内部:
我们发现:Model类型参数的解析方式和Map类型参数的解析是一样的。都是放在了BindAwareModelMap对象里面。并且都是同一个对象(因为内存地址都是一样的)
我们的参数解析完成之后,后续怎么处理?
我们先在InvocableHandlerMethod方法上的doInvoke方法上打一个断点。
然后在目标方法上打一个断点:
然后在ServletInvocableHandlerMethod类的invokeAndHandle上打一个断点:
我们发现:当使用反射的机制调用我们的目标方法之后,进入到了ServletInvocableHandlerMethod类中的invokeAndhandle方法里面。
现在我们获得了目标方法的返回值returnValue.也把控制器的参数信息封装到了BindingAwareModelMap里面去了。
我们继续执行这个方法:
我们step into进来:
我们F8执行完这个方法,回到this.returnValueHandlers.handleResturnValue,继续step into:
总结:
目标方法处理完成以后,将所有的数据都放在 ModelAndViewContainer,包含要去的页面地址View。还包含Model数据。
整个返回值处理完成之后,我们按F8进入RequestMappingHandlerAdapter中invokeHandlerMethod方法。
我们step into进入这个方法。
我们发现mavContainer.getModel(),获取mavContainer里面的数据,然后封装到ModelAndView里面。
到此相当于DispatcherServlet类中mv = ha.handle(processedRequest, response, mappedHandler.getHandler());的流程就全部结束。
当我们的目标方法被处理器处理成ModelAndView之后,后续处理是什么样的?
我们step into applyPostHandle方法:
我们直接F8执行,进入如下方法:
这个processDispatchResult就是处理派发结果。它一定会把我们的数据放在作用域中。
我们step into进入render方法:
我们step into进入resolveViewName方法:
step into 进入resolveViewName方法:
我们按F8直接执行:
我们回到DispatcherServlet类中的render方法:
我们step into进入render方法:
我们step into进入createMergedOutputModel方法:
我们发现它把model里面的数据重新放在了mergeModel里面去了。mergeModel是一个LinkedHashMap类型。
我们继续F8。进入AbstractView类的render方法。
step into进入renderMergedOutputModel方法:
我们继续step into:
结论:
到这里我们终于证明了无论是Map 还是Model它们是同一个东西,最后它们把数据都放在了request作用域中。我们获取数据的时候,也是从Request作用域中获取的。
2.2.10 web开发–pojo参数封装原理
在springboot中,我们通过表单提交参数,在控制器方法中可以通过pojo来封装。那么springboot封装pojo参数的原理是什么?我们来探究一下原理。
- 定义表单
<!--测试表单pojo-->
<form action="/saveuser" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
宠物姓名:<input name="pet.name" value="橘猫"/><br/>
宠物年龄:<input name="pet.age" value="5"/>
<input type="submit" value="保存"/>
</form>
- 定义实体类
@Data
public class Pet {
private String name;
private Integer age;
}
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
- 控制器方法
@PostMapping("/saveuser")
public Person saveuser(Person person){
return person;
}
- 测试:
接下来,我们来探究一下底层执行原理,首先我们把之前的执行流程复习一下:
首先从DispatcherServlet类中的doDispatch方法开始:
点击step into进入这个方法:
先获取到这个handler。
返回到ha.handle后,点击step into继续进入这个方法:
点击step into:
点击step into:
点击step into:
点击step into:
点击step into:
点击step into:
点击step into:
直接放行断点,查看最合适的参数解析器。
结论:ServletModelAttributeMethodProcessor这个参数解析器能够对pojo参数进行解析。
点击step into:
点击step into:
进入ModelAttributeMethodProcessor类中的resolveArgument方法:
这里面有重要的步骤:
我们执行this.createAttribute方法,发现了一个非常重要的点:
我们发现,这个方法为我们创建了一个空的Person对象。
我们继续F8执行代码:
我们继续F8执行代码:
当我们执行this.bindRequestParameters(binder, webRequest);这个方法之后,我们查看debugg信息:
接下来,我们就看看这个参数绑定的方法到底是怎么执行的:
step into进入方法:
点击bind方法进入:
我们看看mpvs封装了什么:
我们发现mpvs里面封装了参数名称和参数值的键值对。
我们F8继续跟踪代码:
点击step into:
继续点击F8:
点击step into进入doBind方法:
点击step into进入doBind方法:
step into进入applyPropertyValues方法:
点击进入setPropertyValues方法:
点击F8:
点击step into:
这个pv里面封装的就是原生的属性和属性值:
step into,进入nestedPa.setPropertyValue(tokens, pv)方法:
step into进入这个方法,然后点击F8再执行:
step into进入this.convertforProperty方法:
step into:
getType():获取要转换的目标数据类型
继续step into:
step into:
我们继续F8
我们step into进入convert方法:
step into:
继续step into:
step into进入getConverter方法:
step into进入convert方法:
2.2.11 web开发–自定义converter原理
通过上一节,我们明白,我们在封装pojo对象的时候,springboot会给我们提供124个数据类型转换器,帮助我们进行数据类型的转换。
那我们能不能自己定义类型转换器呢?其实是可以的。
- 定义html
<!--测试自定义类型转换器-->
<form action="/saveuser2" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
宠物:<input name="pet" value="橘猫-3"/><br/>
<input type="submit" value="保存"/>
</form>
- 定义实体类
@Data
public class Pet {
private String name;
private Integer age;
}
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
- 定义控制器方法
@PostMapping("/saveuser2")
public Person saveuser2(Person person){
return person;
}
- 测试
原因:前台提交的“橘猫-3”数据,springboot没有转换器支持将String类型的数据转换成Pet类型。
如何解决:
自定义数据类型转换器。手动的将String类型的数据转换成Pet类型。
由于springboot支持组件定制化。我们只需要定义一个类实现WebMvcConfigurer接口即可自己定制化组件。
@Configuration
public class HiddenHttpMethodConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) { //source 页面提交过来的值
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split("-");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
}
我们重启再次测试:
那么它的底层原理是什么样的呢?
我们打断点进行调试:
在ServletModelAttributeMethodProcessor参数解析器的resolveArgument解析参数的方法中:
我们通过createBinder方法创建了web参数绑定器。我们指定binder参数绑定器里面封装了数据类型转换器(124个):
我们发现,现在多了一个类型转换器,为什么?因为我们自定义的数据类型转换器生效了。
我们在自己定义的转换器方法上打一个断点:
我们直接断点放行:
到这里,通过源码追踪,我们自定义类型转换器就生效了。
2.2.12 web开发–返回值处理器原理
我们先来定义一个控制器方法:
@RestController
public class ResponseController {
@RequestMapping("test/person")
public Person person(){
Person person = new Person();
person.setUserName("张三");
person.setAge(12);
person.setBirth(new Date());
Pet pet = new Pet();
pet.setName("铁蛋");
pet.setAge(2);
person.setPet(pet);
return person;
}
}
测试:
我们发现,虽然我们控制器方法的返回值是pojo类型,但是给我们响应的json类型的数据。
为什么会是这样子?我们有必要来研究springboot控制器方法的返回值处理原理。
我们从DispatcherServlet类的ha.handle方法为入口:
我们step into进入RequestMappingHandlerAdapter类的invokeHandlerMethod执行目标方法:
我们可以看看具体的返回值处理器:
我们继续F8执行代码:
我们step into进入这个方法:
我们看看返回值的什么类型:
当得到返回值以后,我们继续F8:
this.getReturnValueType(returnValue)
获取返回值的类型
我们step into进入handleReturnValue方法:
this.selectHandler获取返回值处理器方法。那么是如何获取的呢?
我们step into:
这个HandlerMethodReturnValueHandler是什么?其实就是返回值处理器。它是一个接口:
我们不断放行断点:
其实这个RequestResponseBodyMethodProcessor就是我们想要的返回值处理器:
我们step into:
public boolean supportsReturnType(MethodParameter returnType) {
return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class);
}
通过以上代码我们发现:返回值的处理逻辑就是判断当前方法的返回值有没有被@ResponseBody注解修饰。
到目前为止我们确定了如果我们的返回值有@ResponseBody注解修饰。那么我们的返回值处理器就是RequestResponseBodyMethodProcessor。
2.2.13 web开发–返回值处理原理
上一节我们研究了springboot控制器方法的返回值如果使用了@ResponseBody注解修饰,那么返回值的处理是交给RequestResponseBodyMethodProcessor返回值处理器来处理的。那么这个返回值处理器到底是怎么将返回值处理成json数据的呢?我们来探究一下具体的原理。
我们首先把之前的断点全部清除,重新debugg将断点打在HandlerMethodReturnValueHandlerComposite类上面的handleReturnValue方法上:
我们step into进入这个方法:
this.writeWithMessageConverters方法就是将pojo对象转换成json数据的方法
我们step into:
我们继续F8:
什么是内容协商,当我们发送一个请求,在浏览器里面就可以看到:
我们可以debugg控制台看看浏览器和服务器分别支持的内容类型:
如果服务器响应的内容类型支持被浏览器接收,就把这些内容类型放在mediaTypesToUse这个List集合里面。
mediaTypesToUse.add(this.getMostSpecificMediaType(mediaType, producibleType));
我们继续F8:
当我们获取到了mediaTypesToUse之后(服务器可以响应给浏览器的内容类型)。接下来我们继续F8:
convert是什么?就是消息转换器。它其实是一个接口:
响应:Person对象转为JSON。 canWrite方法。
请求: JSON转为Person。 canRead方法。
通过上面的源码追踪得知:我们需要循环遍历我们的内容转换器,那我们的内容转换器到底有多少呢?
我们循环遍历:
最终 MappingJackson2HttpMessageConverte支持把对象转为JSON。
我们step into进入canWrite方法,看看它是如何支持内容转换的:
我们继续step into:
我们进入AbstractJackson2HttpMessageConverter这个类的canWrite方法:
判断完成之后,我们F8结束对canWrite方法的追踪。
我们继续F8执行:
我们step into,查看写的实现细节:
我们step into:
进入AbstractJackson2HttpMessageConverter转换器类的writeInternal方法
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = this.getJsonEncoding(contentType);
OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream, encoding);
try {
this.writePrefix(generator, object);
Object value = object;
Class<?> serializationView = null;
FilterProvider filters = null;
JavaType javaType = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue)object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = this.getJavaType(type, (Class)null);
}
ObjectWriter objectWriter = serializationView != null ? this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer();
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
objectWriter = objectWriter.with(this.ssePrettyPrinter);
}
objectWriter.writeValue(generator, value);
this.writeSuffix(generator, object);
generator.flush();
generator.close();
} catch (InvalidDefinitionException var14) {
throw new HttpMessageConversionException("Type definition error: " + var14.getType(), var14);
} catch (JsonProcessingException var15) {
throw new HttpMessageNotWritableException("Could not write JSON: " + var15.getOriginalMessage(), var15);
}
}
我们F8结束这个方法。
我们看看debugg控制台:
到这里,springboot将pojo以json数据的形式进行相应的原理,我们就讲到这里。
2.2.14 web开发–内容协商原理(基于请求头Accept)
在上节内容中,我们响应了一个pojo,浏览器是以json格式的数据显示的。但是不同的客户端,对响应的数据格式有不同的要求。比如现在客户端想让我们响应xml类型的数据,那应该怎么办?
需求:通过控制器方法,响应一个pojo数据,浏览器以xml的格式进行响应:
- 控制器
@RequestMapping("test/xml")
public Person responseXML(){
Person person = new Person();
person.setUserName("王五");
person.setAge(25);
person.setBirth(new Date());
Pet pet = new Pet();
pet.setName("铁柱");
pet.setAge(4);
person.setPet(pet);
return person;
}
- 添加依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
- 测试
我们发现,springboot可以根据客户端的需求,响应不同的数据信息。那么它的底层到底是如何实现的呢?我们去探究一下底层源码。
我们把断点打在AbstractMessageConverterMethodProcessor类的writeWithMessageConverters方法上面:
由于我们没有指定具体的媒体类型,所以代码会执行到this.getAcceptableMediaTypes(request)方法。
我们step into进入这个方法:
我们发现这个方法内部是通过内容协商管理器来解析媒体类型,那么是如何解析的呢?我们step into进行进入:
我们继续step into,执行resolveMediaTypes方法:
我们继续F8:
我们发现我们获取到Accept的头信息之后,springboot将这些信息包装成了List类型:
我们继续F8.执行结束并跳出这个方法:
当我们通过 this.getAcceptableMediaTypes(request)方法获取了浏览器支持能够接收的媒体类型,接下来我们就要获取服务器能够响应的媒体类型了。
我们执行到this.getProducibleMediaTypes(request, valueType, (Type)targetType);方法。具体如何执行的?我们step into进入这个方法:
我们发现,底层首先获取所有的消息转换器,让后循环遍历消息转换器,判断每种转换器是否支持对我们响应的数据(person)进行媒体类型的支持。如果有支持的媒体类型,直接放在List集合里面。
我们先看看我们的消息转换器:
经过循环遍历,我们发现也就最后4个消息转换器支持将person转换成person和xml.
我们来看看,最后服务器支持的媒体类型(json,xml):
我们回到AbstractMessageConverterMethodProcessor类的writeWithMessageConverters里面:
到现在我们得到了浏览器支持的媒体类型和服务器也能够支持响应的媒体类型。我们继续F8:
我们查看最终支持匹配的媒体类型有:
继续F8:
我们去除重复之后的媒体类型:
继续F8:
经过遍历可知只有MappingJackson2XmlHttpMessageConvert支持将pojo转换成xml。
我们继续F8:
我们发现write方法就是将person以xml格式写出的核心方法:
我们step into:
我们看看给我们设置的响应头:
继续F8:
我们step into,跟踪核心代码:
我们继续F8,执行完flush方法之后:
2.2.15 web开发–内容协商原理(基于请求参数)
上一节我们讨论了基于请求头Accept的内容协商。其实我们也可以自定义内容协商策略。
如果我们要使用基于请求参数的内容协商策略,我们需要先开启这种内容协商策略的支持。
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
发送请求:
http://localhost:8088/test/xml?format=xml
http://localhost:8088/test/xml?format=json
为什么我们拼接了format参数,就能得到我们想要的数据,我们可以追踪一下源码:
我还是打断点进行追踪:
我们把断点打在AbstractMessageConverterMethodProcessor类上的writeWithMessageConverters方法上面:
我们step into:
通过以上我们发现:我们现在的内容协商有两种策略。一种是基于请求参数的内容协商策略。还有一种是基于请求头的协商策略。默认使用的是基于请求参数的内容协商策略。这要求我们请求url上必须携带format参数。那么format参数的值写什么呢?
通过以上发现,format的参数值只能写xml或json。
我们继续step into进入resolveMediaTypes方法:
我们继续step into:
首先执行的是this.getMediaTypeKey(webRequest)这个方法,这个方法是什么意思呢?我们可以点进去看看:
在ParameterContentNegotiationStrategy类里面:
通过观察,我们发现this.getMediaTypeKey(webRequest)就是获取format参数的值。
我们step into 进入resolveMediaTypeKey方法内部:
到这,我们基于请求参数的内容协商原理就讲到这里。
2.2.16 web开发–自定义MessageConverter
上一节,我们可以让控制器方法给我们响应指定的数据,比如json格式的数据,也比如xml格式的数据,那么我们可以不可以让springboot给我们响应自定义类型的数据呢?答案是可以的。
我们知道,springboot支持组件的定制化功能。我们只需要实现WebMvcConfigurer接口,就可以进行组件的定制化。所以我们如果想响应自定义的数据,也可以通过组件的定制化功能来实现。
通过分析,我们只需要定义一个类实现WebMvcConfigurer接口即可。
@Configuration
public class HiddenHttpMethodConfig implements WebMvcConfigurer {
//自定义消息转换器
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyMessageConverter());
}
}
此时我们需要自定义一个消息转换器,对数据进行自定义写出。如何定义消息转换器?我们可以参考源码的实现。
比如springboot自带的MappingJackson2CborHttpMessageConverter(支持将数据以json格式进行响应)。
我们发现,MappingJackson2CborHttpMessageConverter这个消息转换器实现了HttpMessageConverter接口。所以我们自定义的消息转换器也可以实现这个接口。
public class MyMessageConverter implements HttpMessageConverter<Person> {
//是否支持读的方法(此时我们是响应数据,这个方法可以忽略)
@Override
public boolean canRead(Class<?> aClass, MediaType mediaType) {
return false;
}
//判断这个转换器是否支持对指定的数据类型进行写出。
@Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
return aClass.isAssignableFrom(Person.class);
}
/**
* 服务器要统计所有能够写出的媒体类型
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
//将我们自定义的媒体类型放在集合里面。
return MediaType.parseMediaTypes("application/xq");
}
//读的方法(此时我们是响应数据,这个方法可以忽略)
@Override
public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
//自定义响应数据的格式类型
@Override
public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
String data = person.getUserName() + "<-->" + person.getAge() + "<-->" + person.getBirth();
OutputStream body = httpOutputMessage.getBody();
body.write(data.getBytes());
}
}
我们使用postman测试:
那么我们自定义转换器是怎么起的作用?我们可以追踪一下源码。
我们把断点打在AbstractMessageConverterMethodProcessor类的writeWithMessageConverters方法内部:
我们可以step into进入this.getProducibleMediaTypes(request, valueType, (Type)targetType);内部:
我们F8进入else if内部:
我们step into进入getSupportedMediaTypes方法:
我们F8跳出这些方法。
然后我们查看浏览器支持接收的媒体类型(acceptableTypes)和服务器支持响应的媒体类型(producibleTypes):
接下来就要进行服务器和浏览器媒体类型的最佳内容协商。
将匹配的最佳媒体类型放在一个集合(mediaTypesToUse)里面:
我们F8继续:
我们看看有哪些消息转换器:
我们循环遍历:
我们step into看看如何判断支持的:
我们出来自己写的方法,继续F8:
我们step into:
这样我们自定义消息转换器生效的原理我们介绍到这里了。
2.2.17 web开发–自定义基于请求参数的内容协商
同样的,我们可以使用springboot给我们提供的组件定制化的功能帮助我们实现这个功能。
//自定义内容协商策略
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> map = new HashMap<>();
map.put("json",MediaType.APPLICATION_JSON);
map.put("xml",MediaType.APPLICATION_XML);
map.put("xq",MediaType.parseMediaType("application/xq"));
//基于请求参数的内容协商策略
ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(map);
//基于请求头的内容协商策略(springboot默认优先支持基于参数的内容协商策略,此时基于请求头的内容协商策略会失效。如果我们也想支持基于请求头的内容协商策略,我们也可以将这种策略添加进来)
HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy();
//指定支持哪些内容协商策略
configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy,headerContentNegotiationStrategy));
}
现在我们去测试一下:
这样我们基于参数的自定义内容协商就生效了。那么它的底层原理是怎么实现的?
我们还是在AbstractMessageConverterMethodProcessor类上面的writeWithMessageConverters方法上打断点:
点击step into:
点击step into:
我们看看支持的媒体类型:
后续的流程和2.2.15章节的原理一样,在这里就不再演示,大家可以自行研究一下后面的流程。
2.3 模板引擎–thymeleaf
在springmvc中,我们进行视图跳转(重定向、转发)都需要借助于jsp才能实现。但是在springboot中默认不支持jsp。所以我们需要引入第三方模板引擎技术实现页面渲染。
那springboot支持哪些第三方的模板引擎技术呢?
我们可以查看springboot的官方文档(using spring Boot -->starters )
本课程里面,我们给大家介绍的是thymeleaf模板引擎。
thymeleaf官网: https://www.thymeleaf.org/
2.3.1 thymeleaf简介
当然thymeleaf也有自己的缺点,比如在高并发的场景下面,性能问题就比较突出。一般在高并发的场景下面,我们会采用前后端分离的技术。在单体项目中,使用thymeleaf就比较合适。
2.3.2 thymeleaf基本语法
- 表达式
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面片段 |
- 字面量
文本值: ‘one text’ , ‘Another one!’ ;数字: 0 , 34 , 3.0 , 12.3 ;布尔值: true , false
空值: null
变量: one,two,… 变量不能有空格
-
文本操作
字符串拼接: +
变量替换: |The name is ${name}| -
数学运算
运算符: + , - , * , / , %
-
布尔运算
运算符: and , or
一元运算: ! , not -
比较运算
比较: > , < , >= , <= ( gt , lt , ge , le )等式: == , != ( eq , ne )
-
条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
2.3.3 使用thymeleaf
- 引入启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- springboot对thymeleaf的自动配置
自动配好的策略:
- 所有thymeleaf的配置值都在 ThymeleafProperties
- 配置好了 SpringTemplateEngine
- 配好了 ThymeleafViewResolver
- 我们只需要直接开发页面
- 开发页面
使用Thymeleaf,必须引入Thymeleaf命名空间:
xmlns:th="http://www.thymeleaf.org"
- 编写java代码
@Controller
public class ThymeleafController {
@RequestMapping("testTymeleaf")
public String testThymeleaf(Model model){
List<String> list = new ArrayList<>();
list.add("Sunny");
list.add("Kobe");
list.add("Ketty");
model.addAttribute("list",list);
model.addAttribute("message","hello,Thymeleaf");
return "success";
}
}
- 编写html页面
在templates目录下面新建success.html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!--@{}从根目录开始加载资源,类似于jsp里面的${pageContext.request.contextPath}-->
<script th:src="@{/common.js}"></script>
</head>
<body>
<span th:text="${message}"></span>
<!--循环展示-->
<h2>
<div th:each="list:${list}" th:text="${list}"></div>
</h2>
</body>
</html>
- 页面效果
2.3.4 thymeleaf案例–web后台管理案例
2.3.4.1 web后台项目的环境搭建
接来下,我们就搭建一个基于Thymeleaf的web案例,来体验thymeleaf的相关语法。
- 创建springboot工程,引入相关依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
- 创建启动类
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
- 准备静态资源
在resources目录下面,创建static文件夹。将静态资源放在目录下面。
- 准备html页面
将html页面拷贝至templates目录下面。
将login.html页面进行改造:
- 编写控制器代码
@Controller
@Slf4j
public class LoginController {
/**
* 跳转到登录页面 用户请求以/,/login结尾都跳转到login.html页面
* @return
*/
@GetMapping(value = {"/","/login"})
public String loginPage(){
return "login";
}
/**
* 用户登录(重定向跳转,避免使用转发导致表单的重复提交)
* @param username
* @param password
* @return
*/
@PostMapping("/login")
public String main(String username,String password){
return "redirect:/main.html";
}
/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(){
return "main";
}
}
- 启动效果
访问登录页面:
http://localhost:8080/login或者http://localhost:8080/
点击登录,跳转到主页面:
http://localhost:8080/main.html
2.3.4.2 完善登录逻辑
- 完善login.html
给表单控件添加名字:
- 定义pojo
@Data
public class User {
private String userName;
private String password;
}
- 完善登录逻辑
/**
* 用户登录(重定向跳转,避免使用转发导致表单的重复提交)
* @param user
* @return
*/
@PostMapping("/login")
public String main(User user , HttpSession session, Model model){
//如果用户名不为空,并且密码为123456就允许登录
if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误");
//回到登录页面
return "login";
}
}
- 测试
如果用户名、密码输入正确,就跳转到主页
如果用户名、密码输入错误,就跳转到登录页,并展示失败信息。
- 避免用户直接访问主页main.html
用户只有登录之后,才能直接访问主页,如果没有登录直接访问主页,直接强制用户跳转到登录页面。
/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(HttpSession session,Model model){
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return "main";
}else {
//回到登录页面
model.addAttribute("msg","请先登录");
return "login";
}
}
测试:
不登录,直接访问主页:http://localhost:8080/main.html
- 在主页上显示用户信息
在main.html进行页面修改:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
重启项目,测试效果:
2.3.4.3 抽取公共内容
- 将table相关的html页面拷贝至指定的静态资源目录中
在tamplates目录下面建立table目录。然后将指定html页面放在目录下面。
- 定义控制器方法,实现这些页面的跳转:
@Controller
public class TableController {
@GetMapping("/basic_table")
public String basic_table(){
return "table/basic_table";
}
@GetMapping("/dynamic_table")
public String dynamic_table(){
return "table/dynamic_table";
}
@GetMapping("/responsive_table")
public String responsive_table(){
return "table/responsive_table";
}
@GetMapping("/editable_table")
public String editable_table(){
return "table/editable_table";
}
}
- 测试
http://localhost:8080/basic_table
http://localhost:8080/dynamic_table
http://localhost:8080/responsive_table
http://localhost:8080/editable_table
2.3.5 视图解析器与视图原理
上一章节,我们通过thymeleaf模板引擎,实现了页面的跳转。springboot底层肯定离不开视图解析器的处理,那么现在我们讨论一下spingboot底层视图解析器和视图的处理原理。
现在我们打断点,来跟踪一下底层的实现原理:
我们发送一个登录请求。
在DispatcherServlet类上面的doDispatcher方法上打断点:
我们step into,进入handle方法:
继续step into:
继续step into,执行目标方法:
继续step into,执行并处理方法:
继续step into:
继续step into:
我们发现,在进行返回值处理的时候,springboot将返回值放在了一个mavContainer对象里面去了。
当返回值处理完毕之后,我们F8跳出这个方法。
通过这个方法名称,我们得知是返回ModelAndView.ModelAndView对象如何获取?我们step into:
最后我们获取的ModelAndView对象结构如下:
我们F8。跳出mv = ha.handle方法。
我们step into进入this.processDispatchResult方法:
我们step into进入render方法:
它是如何获取视图对象呢?我们step into进入resolveViewName解析视图名称这个方法:
继续step into:
继续step into:
那么到底有哪些视图解析器呢:
经过循环遍历,目前只有ThymeleafViewResolver支持对视图名称进行解析:
我们step into进入resolveViewName方法:
我们step into进入this.createView方法:
我们发现ThymeleafViewResolver视图解析器解析的规则是根据方法的返回值,判断是否是重定向还是转发。如果是重定向(我们控制器方法现在的逻辑就是重定向)就创建一个RedirectView对象。
如果是转发就创建InternalResourceView对象
当我们获取视图之后,我们F8回到DispatcherServlet。进入view.render方法:
我们step into。看看是如何渲染的:
我们step into:
我们继续step into,进入this.sendRedirect方法:
最后我们发现,视图渲染就是使用了原生的servlet帮助我们进行了重定向。
关于thymeleaf如何实现转发的,大家也可以自行的跟踪源码,查看底层实现原理。
2.3.6 拦截器
2.3.6.1 拦截器的使用
在上一个登录案例中,我们只有登录之后,才能访问主页资源。如果用户没有登录,是无法访问到主页资源的。这个功能我们也可以使用拦截器实现。
- 定义拦截器
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
//在目标方法之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);
//登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
//放行
return true;
}
//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
//转发到登录页面
request.getRequestDispatcher("/").forward(request,response);
return false;
}
//在目标方法之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
//在页面渲染之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
- 注册拦截器
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
"/js/**","/aa/**"); //放行的请求
}
}
- 修改后台代码逻辑
修改后台跳转到主页的逻辑:
/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(HttpSession session,Model model){
return "main";
}
2.3.6.2 拦截器的执行原理
上一节,我们使用拦截器完善了登录功能,现在我们讨论一下,拦截器的底层执行流程是什么。
我们在DispatcherServlet类上面的doDispatch上面打断点:
this.getHander获取Handler方法。我们看看它的返回值:
我们发现,当我们获取Handler之后,我们方法的返回值返回了mappedHandler.它是HandlerExecutionChain类型。
我们发现它包含了三个拦截器。一个是我们自定义的拦截器,还有两个是springmvc自带的拦截器。
我们继续F8:
我们step into进入applyPreHandle方法:
通过源码观测我们发现:
applyPreHandle方法的执行逻辑是:会将所有拦截器循环遍历,如果拦截器的preHandle方法为true,就会直接放行,进入下一个拦截器,执行preHandle方法。
如果某一个拦截器的preHandle方法执行为false。就会进入if方法,执行this.triggerAfterCompletion方法。那么this.triggerAfterCompletion做了什么呢?我们可以点击进去看看:
总结:
1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行 所有拦截器的 preHandle方法
- 2.1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
- 2.2、如果当前拦截器返回prehandler为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个拦截器返回false。直接跳出不执行目标方法。
我们继续F8:
当我们执行ha.handle方法(执行目标方法)之后,又调用了applyPostHandler方法。我们step into:
总结:
1、倒序执行所有拦截器的postHandle方法。
2、前面的步骤有任何异常都会直接倒序触发 afterCompletion
this.processDispatchResult方法是在处理派发结果,我们step into:
triggerAfterCompletion方法如何执行,我们可以点击进去看看:
总结:
页面成功渲染完成以后,也会倒序触发 afterCompletion。
到这里,我们拦截器的执行原理就讲解完毕。
2.3.7 文件上传
2.3.7.1 文件上传功能实现
- 引入相关的html页面
- 编写后台控制器代码
@Controller
@Slf4j
public class FormController {
/**
* 跳转到文件上传的页面
*/
@GetMapping("/form_layouts")
public String form_layouts(){
return "form/form_layouts";
}
/**
* 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()){
//保存到文件服务
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("F:\\upload\\"+originalFilename));
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("F:\\upload\\"+originalFilename));
}
}
}
return "main";
}
}
- 测试
2.3.7.2 文件上传原理分析
上一节我们实现了文件上传的功能,那么springboot中,文件上传的具体原理是怎么样的呢?我们来探究一下:
我们先登录,跳转到对应的文件上传页面,然后在DispatcherServlet的doDispatcher方法上面打断点,最后提交文件上传请求。
我们继续F8,执行代码:
springboot如何判断我们的请求是一个文件上传的请求?我们step into进入this.checkMultipart方法:
我们可以看看isMultipart方法如何判断我们的请求是否是一个文件上传请求的,我们step into:
这就要求我们的表单上必须添加一个属性:
我们F8,继续执行代码:
我们的文件上传解析器到底是什么东东?
我们发现,我们的文件上传解析器就是StandardServletMultipartResolver。
如何解析的?我们step into:
我们发现,这个方法就是将我们原生的请求包装成了一个StandardMultipartHttpServletRequest对象。
当我们的请求被判定为是文件上传的请求之后,我们继续F8,到ha.handle方法:
我们step into:
我们发现this.handleInternal方法里面的request对象就是我们包装过的StandardMultipartHttpServletRequest对象。
我们继续step into,进入this.handleInternal方法。
继续step into进入this.invokeHandlerMethod方法:
我们可以大致猜测一下,我们的参数被那个参数解析器解析:
我们继续F8:
我们step into进入invokeAndHandle方法:
invokeForRequest方法是解析请求,并获取目标方法的返回值。我们继续step into:
如何获取方法的参数值?我们继续step into进入getMethodArgumentValues方法:
如何支持的?我们step into进入supportsParameter方法:
我们继续step into:
我们看看参数解析器是如何支持对文件上传参数的解析的:
我们发现RequestPartMethodArgumentResolver参数解析器解析参数的规则是判断我们的参数是否被@RequestPart注解修饰。
我们F8,退出判断是否支持参数解析的方法。
当我们发现使用RequestPartMethodArgumentResolver参数解析器解析支持对文件上传请求参数的解析之后,我们再看看,这个参数解析器是如何解析请求参数的,我们step into进入this.resolvers.resolveArgument方法:
我们继续step into进入resolveArgument方法:
继续step into进入MultipartResolutionDelegate.resolveMultipartArgument这个方法:
继续step into:
这个Map的结构是怎么样的?
总结:
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
-
自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
-
原理步骤
-
- 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 2、参数解析器来解析请求中的文件内容封装成MultipartFile
- 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
2.3.8 异常处理
2.3.8.1 springboot默认异常处理机制
springboot继承了thymeleaf,默认的错误处理规则是去templates目录下面寻找指定错误代码的页面。error/下的4xx,5xx页面会被自动解析。
- 创建错误处理页面
- 模拟异常
@GetMapping("/basic_table")
public String basic_table(){
int i = 10 /0;
return "table/basic_table";
}
- 测试(分别测试404 500的情况)
2.3.8.2 异常处理–底层组件功能分析
上一节我们知道了springboot错误处理的默认规则,那么springboot底层如何进行错误处理的?首先我们需要了解springboot底层进行错误处理的组件有哪些。
首先我们打开springboot错误处理的自动配置类。
我们打开ErrorMvcAutoConfiguration.
自动配置类里面有几个组件我们需要关注一下:
(1) DefaultErrorAttributes
我们发现ErrorMvcAutoConfiguration内部会向容器注入一个id为errorAttributes的DefaultErrorAttributes类型的组件。那么这个组件到底有啥用,我们可以点击进去看看。
我们看看DefaultErrorAttributes这个类里面有哪些方法:
getErrorAttributes方法就是定义错误信息,并将错误信息封装到了一个名为errorAttributes的Map集合里面。我们处理错误的页面就可以将这些数据取出并展示出来。
(2) BasicErrorController
在ErrorMvcAutoConfiguration里面还有一个组件BasicErrorController。
我们可以看看BasicErrorController的具体描述。
我们发现,当我们要进行错误处理的时候,springmvc会默认发送一个/error的请求进行相应的错误处理。
我们再看看这个类里面的其他方法:
我们发现有两个方法,对错误信息进行了响应。
errorHtml:当我们使用浏览器发送请求的时候,如果出现了错误,就使用这个方法进行错误信息响应,默认响应的是一个名为error的视图。
error:当我们使用其他的方式(post man)发送请求的时候。我们就以json格式进行数据响应。默认将数据封装到ResponseEntity里面。
(3) BeanNameViewResolver
在ErrorMvcAutoConfiguration里面还有一个组件BeanNameViewResolver。
我们看到BeanNameViewResolver是根据视图名称进行解析视图,解析一个名为error的视图。那这个error的视图是什么样的?
我们发现return了this.defaultErrorView。
我们点击进入StaticView,我们发现这样一段描述:
private static class StaticView implements View {
private static final MediaType TEXT_HTML_UTF8;
private static final Log logger;
private StaticView() {
}
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (response.isCommitted()) {
String message = this.getMessage(model);
logger.error(message);
} else {
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(this.getContentType());
}
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>There was an unexpected error (type=").append(this.htmlEscape(model.get("error"))).append(", status=").append(this.htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(this.htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
}
这个render方法就是进行错误页面的具体渲染。
到这里我们就清楚了,BasicErrorController里面使用errorHtml方法响应错误视图的原理,就是去寻找一个名为error的视图进行解析。而error视图是StaticView类型的。通过里面的render方法进行错误视图渲染的。
我们可以看看我们平时出现错误的视图:
(4) DefaultErrorViewResolver
在ErrorMvcAutoConfiguration里面还有一个组件DefaultErrorViewResolver。
我们可以点击进入DefaultErrorViewResolver。
这个类里面有一个处理错误视图的方法:
这就回答了为什么我们定义错误页面的时候,为什么要以错误状态码命名,并且将错误页面放在error目录的原因。
到这里我们就把springboot进行错误处理的几个主要的错误处理的组件介绍清楚了,下一节我们来详细介绍这几个组件是如何协同工作的。
2.3.8.3 异常处理–异常处理流程
上一节,我们讨论了springboot异常处理所需要的几个组件。这一节,我们讨论一下springboot进行异常处理的主要流程。
我们先使用debugg模式启动,然后登录,然后分别在指定的位置打上断点。
我们发送请求: http://localhost:8080/basic_table
我们断点放行到mv = ha.handle
我们继续放行断点:
我们F8继续放行:
我们发现,如果我们的目标方法出现了异常,会进入catch语句被捕获处理。并且目标方法的返回值ModelAndView为Null我们继续F8:
我们在DispatcherServlet类上面的this.processDispatchResult方法上打断点,并将断点放行:
我们发现,不管目标方法有没有出现异常,总会被processDispatchResult方法执行(也就是进行视图处理的方法)。我们看它是如何处理的?
我们step into:
我们发现,虽然我们的目标方法出现了异常,返回值mv为null。但是在this.processHandlerException处理目标方法的异常的时候,仍然将处理结果返回成ModelAndView。
如何处理的?我们step into:
我们看看有哪些异常处理器:
我们发现有两类异常处理器,一个是DefaultErrorAttributes。上一节我们介绍过这个异常处理器,它是springboot默认配置的异常处理器。还有一类是组合异常处理器,有分为三个处理器。我们现在分别使用这4个异常处理器进行处理:
我们发现所有异常处理器处理之后,都需要返回一个ModelAndView。
首先我们看看默认异常处理器DefaultErrorAttributes如何进行异常处理的:
我们resolver.resolveException上面step into:
我们发现这个方法将错误信息封装之后,返回了一个null值。也就是使用DefaultErrorAttributes异常处理器处理之后,处理的结果为Null。
this.storeErrorAttributes如何封装异常信息的?继续step into:
我们发现就是将异常信息封装到request作用域。
我们F8.将代码放行到:
我们看看HanlderExceptionResolverComposite如何处理的?
我们step into:
这3个异常处理器我们刚刚看到过:
通过上面代码我们看出也就是分别遍历:
ExceptionHandlerExceptionResolver(方法上标注了@ExceptionHandler注解才解析)
ReponseStatusExceptionResolver(方法上标注了@ReponseStatus注解才解析)
DefaultHandlerExceptionResolver帮助我们进行异常处理。具体的处理逻辑我们不看了。
需要注意的是,这些异常处理器都实现了HandlerExceptionResolver接口:
这就给了我们一个启示:如果我们要自定义异常处理,就需要实现这个接口里面的处理异常的方法。
结论:这个三个异常处理器最后处理的结果都为Null。也就是ModelAndView为null。
此时我们放行断点:
我们发现当所有的错误异常处理器都不能处理的时候(目标方法的返回值ModelAndView为null)。此时默认会转发到/error请求。
我们把断点放行到如下位置:
观察目前处理的是哪个Handler:
我们发现目前请求的是BasicErrorController里面的errorHtml方法。
BasicErrorController是什么?我们上一节讨论过这个类。里面分别定义了响应html错误页面和json信息的方法。
我们断点执行到如下位置:
我们在BasicErrorController里面的errorHtml方法打上断点。
我们放行断点:
此时会进入BasicErrorController里面的errorHtml方法。
我们发现在执行this.resolveErrorView方法之前,将所有的错误信息放在了一个Map里面,我们可以看看:
我们step into进入this.resolveErrorView方法:
我们发现和之前一样,还是遍历所有的异常处理器进行异常处理,那么到底有哪些异常处理器呢?
我们发现只有一个异常处理器,那就是DefaultErrorViewResolver。而这个错误视图解析器我们上一节给大家介绍过。
我们看看它是如何进行错误视图解析的:
我们step into进入resolver.resolveErrorView方法:
到这里,我们springboot默认的异常处理的整个流程就给大家介绍到这了。
2.3.8.4 springboot其他处理异常的方式
第一种:@ControllerAdvice + @ExceptionHandler注解
@Slf4j
@ControllerAdvice
public class GlobalExceptionResolver {
@ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //处理异常
public String handleArithException(Exception e){
log.error("异常是:{}",e);
return "login"; //视图地址
}
}
那么它的底层处理逻辑是什么呢?
我们首先在DispatcherServlet上面的方法handle上打上断点:
我们在出现异常的控制器方法上打上断点,并放行:
我们F8执行代码,由于方法出现了异常,就会进行异常的捕获处理:
我们在DispatcherServlet上面的this.processDispatchResult方法上打上断点,并放行断点到这里:
我们step into,看看底层对目标方法进行视图处理的结果:
继续step into:
首先是DefaultErrorAttribute进行处理,处理之后的ModelAndView为null。
然后继续循环遍历:
前面可知,这个异常处理器包含三个小的异常处理器:
所以其内部处理逻辑应该是遍历这三个异常处理器挨个处理:
我们step into看看处理流程:
我们发现首先是由ExceptionHandlerExceptionResolver来处理的,这个异常处理器也是能够处理的。我们step into:
我们在自己处理异常的方法打上断点,然后放行断点至如下地方:
我们发现我们自定义的异常处理方法执行了。是由ExceptionHandlerExceptionResolver这个异常处理器处理的。我们继续F8:
我们放行断点至ExceptionHandlerExceptionResolver类的如下地方:
我们继续F8,将代码执行到DispatcherServlet类:
当我们通过异常解析器解析自定义异常方法拿到视图之后,下面就是渲染的流程了(render方法)。
第二种:自定义异常处理
@ResponseStatus(value= HttpStatus.BAD_GATEWAY,reason = "id不能为负数")
public class CustomizeException extends RuntimeException{
public CustomizeException(){}
public CustomizeException(String message){
super(message);
}
}
@GetMapping("/testCustomizeException")
public String testCustomizeException(@RequestParam("id") Integer id){
if(id < 0){
throw new CustomizeException();
}
return "table/dynamic_table";
}
发送请求:http://localhost:8080/testCustomizeException?id=-1
我们现在打断点测试其底层执行流程:
我们step into:
继续step into:
首先是DefaultErrorAttributes进行解析,解析的ModelAndView为null。交给下一个异常解析器HandlerExceptionResolverComposite进行解析。
这三个解析器我们已经很熟悉了:
首先是由ExceptionHandlerExceptionResolver解析,但是这个异常解析器解析的是@ControllerAdvice + @ExceptionHandler注解的异常。所以这个异常解析器不会解析我们自定义的异常(被@ResponseStatus注解修饰的异常)。
接下来我们放行断点,交给ResponseStatusResolver进行解析。
我们step into:
这个方法是如何解析的?我们step into:
此时会进入到ResponseStatusExceptionResolver类里面:
此时会判断是否存在一个被注解@ResponseStatus注解修饰的类。如果存在就进入if语句里面。
我们继续step into:
此时会先获取错误的状态码,错误的原因,然后调用applyStatusAndReason方法,并将状态码和异常信息作为参数传递进去。
我们继续step into:
我们发现applyStatusAndReason方法内部将错误状态码和错误异常信息拿到了之后,传递到sendError方法中作为参数。然后调用sendError方法。最后返回了一个空的ModelAndView对象。
注意:只要调用了sendError方法,意味着这次请求执行结束,然后tomcat服务器会发送一个/error请求。
我们放行断点:
第三种:框架底层对异常的处理
由于springmvc底层对很多异常进行了处理,我们就抛砖引玉,拿出一种进行讲解。
我们之前定义了一个controller:
@GetMapping("/testCustomizeException")
public String testCustomizeException(@RequestParam("id") Integer id){
if(id < 0){
throw new CustomizeException();
}
return "table/dynamic_table";
}
控制器方法需要接收一个参数id才能请求,现在我们不带参数进行请求:http://localhost:8080/testCustomizeException
我们打断点,将请求放行到如下:
我们可以查看捕获的异常类型是:
我们step into:
继续step into:
DefaultErrorAttributes进行处理,很显然不能处理。放行交给其他的异常处理器处理:
我们step into进入:
我们循环遍历三个异常处理解析器,直接放行到第三个异常处理器进行处理,也就是DefaultExceptionResolver
我们继续step into:
继续step into:
我们进入DefaultHandlerExceptionResolver类里面:
doResolveException方法里面罗列了框架需要底层需要处理的所有异常:
@Nullable
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof HttpRequestMethodNotSupportedException) {
return this.handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException)ex, request, response, handler);
}
if (ex instanceof HttpMediaTypeNotSupportedException) {
return this.handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException)ex, request, response, handler);
}
if (ex instanceof HttpMediaTypeNotAcceptableException) {
return this.handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException)ex, request, response, handler);
}
if (ex instanceof MissingPathVariableException) {
return this.handleMissingPathVariable((MissingPathVariableException)ex, request, response, handler);
}
if (ex instanceof MissingServletRequestParameterException) {
return this.handleMissingServletRequestParameter((MissingServletRequestParameterException)ex, request, response, handler);
}
if (ex instanceof ServletRequestBindingException) {
return this.handleServletRequestBindingException((ServletRequestBindingException)ex, request, response, handler);
}
if (ex instanceof ConversionNotSupportedException) {
return this.handleConversionNotSupported((ConversionNotSupportedException)ex, request, response, handler);
}
if (ex instanceof TypeMismatchException) {
return this.handleTypeMismatch((TypeMismatchException)ex, request, response, handler);
}
if (ex instanceof HttpMessageNotReadableException) {
return this.handleHttpMessageNotReadable((HttpMessageNotReadableException)ex, request, response, handler);
}
if (ex instanceof HttpMessageNotWritableException) {
return this.handleHttpMessageNotWritable((HttpMessageNotWritableException)ex, request, response, handler);
}
if (ex instanceof MethodArgumentNotValidException) {
return this.handleMethodArgumentNotValidException((MethodArgumentNotValidException)ex, request, response, handler);
}
if (ex instanceof MissingServletRequestPartException) {
return this.handleMissingServletRequestPartException((MissingServletRequestPartException)ex, request, response, handler);
}
if (ex instanceof BindException) {
return this.handleBindException((BindException)ex, request, response, handler);
}
if (ex instanceof NoHandlerFoundException) {
return this.handleNoHandlerFoundException((NoHandlerFoundException)ex, request, response, handler);
}
if (ex instanceof AsyncRequestTimeoutException) {
return this.handleAsyncRequestTimeoutException((AsyncRequestTimeoutException)ex, request, response, handler);
}
} catch (Exception var6) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", var6);
}
}
return null;
}
我们直接跳转到我们现在需要处理的异常:
我们继续step into:
我们发现,框架底层在进行异常处理的时候,也是调用sendError方法,发送一个error请求。
我们放行断点:
到这里,springmvc框架底层进行异常解析的流程我们讲到这里。
第四种:自定义异常处理器,实现HandlerExceptionResolver接口
我们发现spring提供的异常处理器都实现了HandlerExceptionResolver接口。我们也可以自定义异常类实现这个接口:
@Order(value = Ordered.HIGHEST_PRECEDENCE) //设置最高优先级(优先自定义的异常解析器生效),因为springmvc默认的异常处理方式为优先生效。
@Component
public class MyExceptionHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object o, Exception ex) {
ModelAndView mv = new ModelAndView();
try {
response.sendError(555,"出现了错误!!!!");
} catch (IOException e) {
e.printStackTrace();
}
return mv;
}
}
我们打断点,看看我们自定义异常处理器会不会生效:
具体的异常处理逻辑请大家结合之前知识,大家可以自行追踪一下异常处理源码。
2.3.9 在springboot中使用原生的web组件
由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件。
注册三大组件用以下方式:
- ServletRegistrationBean 注册Servlet组件
- FilterRegistrationBean 注册Filter组件
- ServletListenerRegistrationBean 注册监听器组件
2.3.9.1 在web容器中注册web三大组件
(1) 注册servlet组件
- 定义servlet
@WebServlet("/demo1")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("servlet方法执行了....");
}
}
- 启动测试:
我们发现直接定义servlet去访问,我们的servlet并没有被识别。
- 使用ServletRegistrationBean 注册Servlet组件
@Configuration
public class MyConfig {
//注册Servlet组件
@Bean
public ServletRegistrationBean registerServlet(){
ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),"/demo1");
return registrationBean;
}
}
- 测试
http://localhost:8080/demo1
(2) 注册Filter组件
- 定义过滤器类
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter执行了.....");
filterChain.doFilter(request,response);
}
}
- 使用FilterRegistrationBean 注册Filter组件
//注册Filter组件
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean filterBean = new FilterRegistrationBean();
filterBean.setFilter(new MyFilter());
filterBean.setUrlPatterns(Arrays.asList("/demo1"));
return filterBean;
}
- 测试
访问:http://localhost:8080/demo1
(3) 注册Listener组件
- 定义监听器
public class MyListener implements ServletContextListener {
//监听SerlvetContext对象的创建
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ServletContext对象创建了.....");
}
//监听SerlvetContext对象的销毁
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ServletContext对象销毁了.....");
}
}
- 使用ServletListenerRegistrationBean 注册监听器组件
//注册Listener组件
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean(){
ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
return registrationBean;
}
- 测试
当启动springboot项目,监听器的监听servlet上下文对象的方法执行了:
当销毁spring容器的时候,监听器销毁servlet上下文对象的方法执行了:
2.3.9.2 DispatcherServlet注册原理
上一节我们讲到了我们定义的servlet是交给ServletRegistrationBean进行注入的。springmvc的前端控制器DispatcherServlet也是一个servlet,那么这个servlet是如何注入到servlet容器的呢?
我们打开关于servlet的自动配置类DispatcherServletRegistrationConfiguration。
我们发现,会将配置文件的配置信息绑定到WebMvcProperties类上面。我们点击进入WebMvcProperties这个类上面看看:
我们继续看:
具体源代码如下:
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
registration.setName("dispatcherServlet");
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
我们发现DispatcherServlet是被DispatcherServletRegistrationBean注入到容器里面了。那么DispatcherServletRegistrationBean又是啥?我们点击进去看看:
我们发现DispatcherServletRegistrationBean继承了ServletRegistrationBean。
webMvcProperties.getServlet().getPath()就是获取DispatcherServlet默认的请求路径:
如果我们想修改DispatcherServlet的默认映射路径,可以使用如下配置:
# /boot就是修改之后的映射路径
spring.mvc.servlet.path=/boot
总结:
通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。默认映射的是 / 路径。
2.3.10 springboot嵌入式web容器
2.3.10.1 springboot嵌入式servlet容器原理
关于springboot嵌入式servlet容器的介绍,spring官网也做了介绍:
上面的这段话翻译之后,大致是这个意思:
在底层,Spring Boot 使用不同类型的ApplicationContext
嵌入式 servlet 容器支持。ServletWebServerApplicationContext是一种特殊的ApplicationContext。通常通过TomcatServletWebServerFactory、JettyServletWebServerFactory或者UndertowServletWebServerFactory来进行加载。
上面介绍,springboot使用ServletWebServerApplicationContext作为servlet容器,我们可以看看其源码:
public class ServletWebServerApplicationContext extends GenericWebApplicationContext implements ConfigurableWebServerApplicationContext {}
我们观察其架构图:
我们发现ServletWebServerApplicationContext其实是继承了ApplicationContext接口,它确实是一个web容器。
在这里我们有必要搞清楚springboot初始化ServletWebServerApplicationContext
的具体细节。
当我们springboot项目导入web启动器之后,web应用会创建一个ServletWebServerApplicationContext
容器,ServletWebServerApplicationContext启动的时候,会找ServletWebServletFactory。
关于ServletWebServletFactory工厂有很多,比如TomcatServletWebServerFactory
, JettyServletWebServerFactory
, or UndertowServletWebServerFactory
。
需要注意的是这些ServletWebServletFactory我们都不用配,springboot底层会给我们自动进行这些工厂的配置。
springboot通过一个ServletWebServerFactoryAutoConfiguration
配置类给我们配置好了这些ServletWebServletFactory。
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
EmbeddedTomcat.class,
EmbeddedJetty.class,
EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {}
我们发现ServletWebServerFactoryAutoConfiguration
分别导入了EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class。
我们点击EmbeddedTomcat.class。发现跳转到了ServletWebServerFactoryConfiguration
我们看这个类里面的内容:
@ConditionalOnClass({Servlet.class, Undertow.class, SslClientAuthMode.class})
@ConditionalOnMissingBean(
value = {ServletWebServerFactory.class},
search = SearchStrategy.CURRENT
)
static class EmbeddedUndertow {
EmbeddedUndertow() {
}
@Bean
UndertowServletWebServerFactory undertowServletWebServerFactory(ObjectProvider<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers, ObjectProvider<UndertowBuilderCustomizer> builderCustomizers) {
UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
factory.getDeploymentInfoCustomizers().addAll((Collection)deploymentInfoCustomizers.orderedStream().collect(Collectors.toList()));
factory.getBuilderCustomizers().addAll((Collection)builderCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Servlet.class, Server.class, Loader.class, WebAppContext.class})
@ConditionalOnMissingBean(
value = {ServletWebServerFactory.class},
search = SearchStrategy.CURRENT
)
static class EmbeddedJetty {
EmbeddedJetty() {
}
@Bean
JettyServletWebServerFactory JettyServletWebServerFactory(ObjectProvider<JettyServerCustomizer> serverCustomizers) {
JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
factory.getServerCustomizers().addAll((Collection)serverCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Servlet.class, Tomcat.class, UpgradeProtocol.class})
@ConditionalOnMissingBean(
value = {ServletWebServerFactory.class},
search = SearchStrategy.CURRENT
)
static class EmbeddedTomcat {
EmbeddedTomcat() {
}
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory(ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getTomcatConnectorCustomizers().addAll((Collection)connectorCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatContextCustomizers().addAll((Collection)contextCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatProtocolHandlerCustomizers().addAll((Collection)protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}
}
我们发现,这个类里面都配置了UndertowServletWebServerFactory、JettyServletWebServerFactory、TomcatServletWebServerFactory这些工厂类。这就正好应证了官网的描述。
但是注意的是,这个类在注入的时候,都是按照条件进行注入的,也就是@ConditionalOnClass({Servlet.class, Tomcat.class, UpgradeProtocol.class})。
也就是你只有导入对应的服务器的包,才会注入对应的服务器工厂。比如我们导入的是tomcat的包,就只会注入TomcatServletWebServerFactory。
一句话总结:ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory。
搞清楚这些原理了,我们就可以看看springboot的web容器是怎么初始化的了。我们回到ServletWebServerApplicationContext类:
我们在createWebServer方法上打上断点:
我们发现使用getWebServerFactory方法获取ServletWebServletFactory:
我们发现factory.getWebServer就是创建web服务器。如何创建的?我们可以step into进入factory.getWebServer方法:
最后初始化的web服务器是什么样的?
2.3.10.2 自定义嵌入式web容器
springboot内置的web容器是tomcat服务器,如果我们不想使用,使用别的web容器。我们也可以自定义。比如jetty服务器:
<dependencies>
<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-jetty</artifactId>
</dependency>
</dependencies>
我们启动项目,观察启动日志:
2.4 springboot整合数据持久层
我们知道,springboot只要导入了某个场景的启动器,当springboot项目启动之后,就会进行这个场景的自动配置。如果在springboot项目中我们想整合数据持久层,我们只需要导入数据持久层相关的启动器即可。
我们通过官网,来看看springboot官方给我们提供了哪些持久层的启动器:
我们发现,这些以spring-boot-starter-data开头的都是关于数据持久层场景有关的启动器。
那么现在我们来看看,如果我们要整合最常用的数据持久层技术jdbc,我们在springboot中应该如何做?
2.4.1 springboot整合jdbc
通过官网,我们发现,我们只需要在pom.xml文件中导入spring-boot-starter-data-jdbc启动器即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
我们打开这个启动类看看它的依赖详细信息:
现在我们有个疑问,为什么springboot没有给我们导入数据库驱动的依赖?
因为springboot不知道我们使用什么数据库,我们需要自己根据实际情况,导入对应的数据库驱动。
比如我们使用的是mysql数据库,我们需要将mysql相关的驱动依赖导入到pom.xml中即可。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
现在不写数据库群的版本号可以吗?
答案是可以的,因为springboot父启动器内部对mysql驱动进行了版本的仲裁。
我们也可以看看我们导入的驱动器依赖的版本号:
但是我们如果本机安装的数据库版本和springboot仲裁的驱动版本不一致,怎么办?比如我们安装的是mysql5.x的版本。但是springboot给我们仲裁的数据库驱动版本是8.0的版本。此时如果直接使用一定会报错。我们需要自己对驱动的版本进行调整。
如何调整?有两种方式:
第一种: 手动加上版本号version
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
当我们手动指定版本号之后,我们依赖的版本就会发生变化:
第二种: 使用properties标签手动指定版本属性
<properties>
<mysql.version>5.1.6</mysql.version>
</properties>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
接下来我们看看,当我们导入spring-boot-starter-data-jdbc启动器之后,springboot进行了jdbc场景的哪些组件的自动配置。
- DataSourceAutoConfiguration自动配置类
(1) 配置信息的自动绑定
我们打开DataSourceAutoConfiguration配置类:
我们发现,DataSourceAutoConfiguration在进行自动配置的时候,开启了配置文件的属性绑定,将配置文件内容绑定到DataSourceProperties上。我们打开DataSourceProperties看看是如何绑定的:
(2) HikariDataSource数据源的自动配置
我们发现PooledDataSourceConfiguration数据源自动配置类里面导入了Hikari、Dbcp等类。我们打开Hikari这个类看看:
我们可以对数据源进行详细的设置:
#配置数据源类型(默认就是这个 可以不写)
#spring.datasource.type=com.zaxxer.hikari.HikariDataSource
#配置数据库用户名
spring.datasource.username=root
#配置数据库密码
spring.datasource.password=Admin123!
#配置连接数据库的url
spring.datasource.url=jdbc:mysql://192.168.10.137:3306/boot2020
#配置驱动类
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
我们可以测试一下:
我们启动启动器App。
我们也可以自己写测试类来测试:
首先的导入一个springboot整合junit的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
@SpringBootTest(classes = App.class)
public class TestJdbc {
@Autowired
DataSource dataSource;
@Test
public void testDataSource(){
System.out.println(dataSource);
}
}
我们发现,springboot默认的数据源就是HikariDataSource数据源。
- DataSourceTransactionManagerAutoConfiguration配置类
事务管理器的自动配置。
我们打开DataSourceTransactionAutoConfiguration:
我们发现在这个自动配置类里面注入了JdbcTransactionManager事务管理器:
我们打开JdbcTransactionManager事务管理器:
public class JdbcTransactionManager extends DataSourceTransactionManager {
....
}
我们发现这个事务管理器继承了DataSourceTransactionManager。而DataSourceTransactionManager就定义了各种事务操作的方法:
- JdbcTemplateAutoConfiguration自动配置类
这个类就是关于JdbcTemplate的自动配置,可以来对数据库进行crud。
@EnableConfigurationProperties({JdbcProperties.class})
@Import({JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class})
public class JdbcTemplateAutoConfiguration {
public JdbcTemplateAutoConfiguration() {
}
}
JdbcTemplateAutoConfiguration这个自动配置类开启了配置文件的自动绑定,将spring.jdbc开头的配置信息进行自动绑定:
@ConfigurationProperties(
prefix = "spring.jdbc"
)
public class JdbcProperties {
....
}
将配置信息自动绑定到JdbcProperties这个类上面:
也就是说我们可以在springboot项目中可以直接使用JdbcTemplate了。
@SpringBootTest(classes = App.class)
public class TestJdbc {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
public void testJdbcTemplate(){
Long count = jdbcTemplate.queryForObject("select count(*) from student", Long.class);
System.out.println(count);
}
}
2.4.2 springboot整合druid数据源
通过上一节我们得知,springboot底层整合的是HikariDataSource数据源。但是在企业开发中,还有一个第三方的数据源也使用比较多。这个数据源就是Druid数据源。
那我们如何使用Druid数据源呢。有两种方式,一种是使用自定义的方式整合druid数据源。第二种是使用starter的方式整合druid数据源。
2.4.2.1 使用自定义的方式整合druid数据源
(1) 创建Druid数据源
- 第一步,导入druid数据源依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
- 定义配置类,将Druid数据源注入到IOC容器里面
@Configuration
public class MyDataSourceConfig {
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource() throws SQLException {
//druidDataSource.setUrl();
//druidDataSource.setUsername();
//druidDataSource.setPassword();
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
}
- 测试
@SpringBootTest(classes = App.class)
public class TestJdbc {
@Autowired
DataSource dataSource;
@Test
public void testDataSource() throws Exception{
System.out.println(dataSource.getConnection());
}
}
控制台输出:
(2) 创建Druid数据源监控
druid数据源监控提供监控信息展示的html页面,也提供监控信息的JSON API。
/**
* 配置 druid的监控页功能
* @return
*/
@Bean
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","123456");
return registrationBean;
}
/**
* WebStatFilter 用于采集web-jdbc关联监控的数据。
*/
@Bean
public FilterRegistrationBean webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
我们测试一下:
我们写一个controller:
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("query")
public @ResponseBody String query(){
Long count = jdbcTemplate.queryForObject("select count(*) from student", Long.class);
return count.toString();
}
发送请求: http://localhost:8080/query
我们发现,监控没有起作用,是因为我们还没有在Druid数据源中开启使用监控的功能。
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource() throws SQLException {
//druidDataSource.setUrl();
//druidDataSource.setUsername();
//druidDataSource.setPassword();
DruidDataSource druidDataSource = new DruidDataSource();
//开启使用数据源监控的功能
druidDataSource.setFilters("stat");
return druidDataSource;
}
2.4.2.2 使用starter方式整合druid数据源
springboot官方给我们提供了一种更加便捷的方式,配置druid数据源。就是直接导入druid相关的启动器即可。我们把上一节关于druid相关的配置类全部注释掉(MyDataSourceConfig这个类不要了)。
我们需要在pom.xml里面导入druid启动器:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
当我们在项目中导入druid-spring-boot-starter,就会开启对druid数据源的自动配置,这个自动配置数据源的类是DruidDataSourceAutoConfigure。
@Configuration
@ConditionalOnClass({DruidDataSource.class})
@AutoConfigureBefore({DataSourceAutoConfiguration.class})
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidSpringAopConfiguration.class, DruidStatViewServletConfiguration.class, DruidWebStatFilterConfiguration.class, DruidFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {
private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);
public DruidDataSourceAutoConfigure() {
}
@Bean(
initMethod = "init"
)
@ConditionalOnMissingBean
public DataSource dataSource() {
LOGGER.info("Init DruidDataSource");
return new DruidDataSourceWrapper();
}
}
- @AutoConfigureBefore({DataSourceAutoConfiguration.class})
在DataSourceAutoConfiguration类生效之前进行配置。因为DataSourceAutoConfiguration里面配置了HikariDataSource数据源。
因为数据源配置是以用户定义优先为原则。只有我们配置了数据源,官方默认的数据源HikariDataSource就不会生效。
- @EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
开启配置文件和DruidStatProperties类的自定绑定。
也就是只要一spring.datasource.druid开头的配置信息都会绑定在DruidStatProperties上。
- @Import({DruidSpringAopConfiguration.class, DruidStatViewServletConfiguration.class, DruidWebStatFilterConfiguration.class, DruidFilterConfiguration.class}) 导入指定的组件。
DruidSpringAopConfiguration:
监控SpringBean的。配置文件信息: spring.datasource.druid.aop-patterns
DruidStatViewServletConfiguration:
监控页的配置。配置文件信息:spring.datasource.druid.stat-view-servlet.enabled,默认是开启的。
@Bean
public ServletRegistrationBean statViewServletRegistrationBean(DruidStatProperties properties) {
StatViewServlet config = properties.getStatViewServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean();
registrationBean.setServlet(new com.alibaba.druid.support.http.StatViewServlet());
registrationBean.addUrlMappings(new String[]{config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"});
if (config.getAllow() != null) {
registrationBean.addInitParameter("allow", config.getAllow());
} else {
registrationBean.addInitParameter("allow", "127.0.0.1");
}
if (config.getDeny() != null) {
registrationBean.addInitParameter("deny", config.getDeny());
}
if (config.getLoginUsername() != null) {
registrationBean.addInitParameter("loginUsername", config.getLoginUsername());
}
if (config.getLoginPassword() != null) {
registrationBean.addInitParameter("loginPassword", config.getLoginPassword());
}
if (config.getResetEnable() != null) {
registrationBean.addInitParameter("resetEnable", config.getResetEnable());
}
return registrationBean;
}
我们发现,这个方法就是将StatViewServlet这个servlet生效了。跟我们上一节在配置类里面注入StatViewServlet组件是一样的作用。
DruidWebStatFilterConfiguration:
web监控配置,默认配置信息:spring.datasource.druid.web-stat-filter.enabled,默认是开启的。
@Bean
public FilterRegistrationBean webStatFilterRegistrationBean(DruidStatProperties properties) {
WebStatFilter config = properties.getWebStatFilter();
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
com.alibaba.druid.support.http.WebStatFilter filter = new com.alibaba.druid.support.http.WebStatFilter();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(new String[]{config.getUrlPattern() != null ? config.getUrlPattern() : "/*"});
registrationBean.addInitParameter("exclusions", config.getExclusions() != null ? config.getExclusions() : "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
if (config.getSessionStatEnable() != null) {
registrationBean.addInitParameter("sessionStatEnable", config.getSessionStatEnable());
}
if (config.getSessionStatMaxCount() != null) {
registrationBean.addInitParameter("sessionStatMaxCount", config.getSessionStatMaxCount());
}
if (config.getPrincipalSessionName() != null) {
registrationBean.addInitParameter("principalSessionName", config.getPrincipalSessionName());
}
if (config.getPrincipalCookieName() != null) {
registrationBean.addInitParameter("principalCookieName", config.getPrincipalCookieName());
}
if (config.getProfileEnable() != null) {
registrationBean.addInitParameter("profileEnable", config.getProfileEnable());
}
return registrationBean;
}
我们发现,这个方法就是将WebStatFilter这个filter生效了。跟我们上一节在配置类里面注入WebStatFilter组件是一样的作用。
DruidFilterConfiguration:
所有Druid自己filter的配置。
根据我们分析的自动配置,我们可以对druid进行详细配置:
#配置数据源类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#配置数据库用户名
spring.datasource.username=root
#配置数据库密码
spring.datasource.password=Admin123!
#配置连接数据库的url
spring.datasource.url=jdbc:mysql://192.168.10.137:3306/boot2020
#配置驱动类
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 监控SpringBean
spring.datasource.druid.aop-patterns=com.xq.*
# 开启stat(sql监控),wall(防火墙)
spring.datasource.druid.filters=stat,wall
# 配置监控页功能
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
# 开启监控web
spring.datasource.druid.web-stat-filter.enabled=true
spring.datasource.druid.web-stat-filter.url-pattern=/*
spring.datasource.druid.web-stat-filter.exclusions='*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
# 对sql监控 防火墙的详细配置
spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.slow-sql-millis=1000
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.config.delete-allow=false
2.4.3 springboot整合mybatis
2.4.3.1 使用纯配置的方式整合mybatis
根据我们之前的经验,我们只需要将mybatis对应的启动器放在pom文件里面即可。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
我们看看这个启动器里面包含哪些内容:
接下来我们看看引入mybatis的启动器之后,mybatis进行了哪些方面的配置,我们打开MybatisAutoConfiguration这个配置类:
- @EnableConfigurationProperties({MybatisProperties.class})
开启mybatis相关配置文件的自动绑定。
- SqlSessionFactory
我们发现也自动配置了SqlSessionFactory。这样我们就不用自己配置SqlSessionFactory了。
- @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
我们打开AutoConfiguredScannerRegistrar:
也就是我们在springboot中,我们的接口可以使用@Mapper注解来管理。
接下来我们就在springboot中定义和mybatis整合相关的配置。
(1) 准备实体类,接口 ,service, controller:
public class Account {
private Integer id;
private String name;
private Double money;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
@Mapper
public interface AccountMapper {
public Account findById(Integer id);
}
注意:根据我们之前的分析,我们的接口必须使用@Mapper注解管理。这样我们接口的代理对象才能被spring容器管理。
public interface AccountService {
public Account findById(Integer id);
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
AccountMapper accountMapper;
@Override
public Account findById(Integer id) {
return accountMapper.findById(id);
}
}
@RestController
@RequestMapping("account")
public class AccountController {
@Autowired
AccountService accountService;
@RequestMapping("findById")
public Account findById(@RequestParam("id") Integer id){
return accountService.findById(id);
}
}
(2) 定义mybatis的核心配置文件,映射文件:
- mybatis核心配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
</configuration>
- mybatis的映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xq.mapper.AccountMapper">
<select id="findById" parameterType="int" resultType="com.xq.pojo.Account">
select * from account where id = #{id}
</select>
</mapper>
- 在application.properties里面定义mybatis的配置文件
# 引入mybatis的核心配置文件
mybatis.config-location=classpath:mybatis/sqlMapConfig.xml
# 引入mybatis的mapper映射文件
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
- 启动测试:
http://localhost:8080/account/findById?id=1
2.4.3.2 使用纯注解的方式整合mybatis
- 定义接口
@Mapper
public interface AccountMapper {
public Account findById(Integer id);
@Select("select * from account")
public List<Account> findAll();
}
- 定义service
public interface AccountService {
public List<Account> findAll();
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
AccountMapper accountMapper;
@Override
public List<Account> findAll() {
return accountMapper.findAll();
}
}
- 定义controller
@RestController
@RequestMapping("account")
public class AccountController {
@Autowired
AccountService accountService;
@RequestMapping("findAll")
public List<Account> findAll(){
return accountService.findAll();
}
}
- 测试
http://localhost:8080/account/findAll
我们每次在接口上都要使用@Mapper注解,每个接口都写这个注解很麻烦,我们可以批量开启对接口的扫描。我们可以注释掉接口上的@Mapper注解。在启动类上使用@MapperScan注解。
@SpringBootApplication
@MapperScan(basePackages = "com.xq.mapper")
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
2.4.4 springboot整合mybatisplus
什么是mybatisplus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
2.4.4.1 整合mybatisplus
我们打开mybatis的官网,根据官网的文档整合即可。官网地址:https://baomidou.com/
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
(1) mybatisplus进行的自动配置
我们现在看看引入mybatis-plus启动器之后,会进行哪些自动配置。
我们打开MybatisPlusAutoConfiguration。
- @EnableConfigurationProperties({MybatisPlusProperties.class})
我们发现通过@EnableConfigurationProperties注解开启了配置文件的自动绑定。
只要配置文件以mybatis-plus开头的配置信息都会自动绑定到MybatisPlusProperties上。
MybatisPlusProperties绑定配置文件的属性值,但是也设置了一些属性的默认值。比如mapperLocations。classpath*:/mapper/**/*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下
- SqlSessionFactory
我们发现,SqlSessionFactory也已经自动配置好了,并且底层使用的是spring容器中的数据源。
- SqlSessionTemplate
我们打开SqlSessionTemplate:
我们可以通过SqlSessionTemplate获取SqlSession对象。
- @Mapper
我们可以通过@Mapper注解或者@MapperScan注解对接口进行扫描。
(2) 搭建mybatisplus环境
- 创建数据表
CREATE TABLE tb_user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
`name` VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
- 创建实体类
@AllArgsConstructor
@NoArgsConstructor
@Data
@TableName("tb_user")
public class User {
/**
* 所有属性都应该在数据库中
*/
@TableField(exist = false) //当前属性表中不存在
private String userName;
@TableField(exist = false)
private String password;
//以下是数据库字段
private Long id;
private String name;
private Integer age;
private String email;
}
- 创建接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
- 测试
@Autowired
UserMapper userMapper;
@Test
public void testFindById(){
User user = userMapper.selectById(1);
System.out.println(user);
}
2.4.4.2 mybatis 之crud操作
(1) 分页查询
- dao
public interface UserMapper extends BaseMapper<User> {
}
- service
public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
- controller
@Controller
public class TableController {
@Autowired
UserService userService;
@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn, Model model){
//构造分页参数
Page<User> page = new Page<>(pn, 2);
//调用page进行分页
Page<User> userPage = userService.page(page, null);
model.addAttribute("users",userPage);
return "table/dynamic_table";
}
}
- 配置分页插件
@Configuration
public class MyBatisConfig {
/**
* MybatisPlusInterceptor
* @return
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
//这是分页拦截器
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setOverflow(true);
paginationInnerInterceptor.setMaxLimit(500L);
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
return mybatisPlusInterceptor;
}
}
2.4.5 springboot整合redis
springboot整合redis,我们只需要引入redis相关的启动器即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
我们看看,springboot对redis进行了哪些相关的配置:
我们打开RedisAutoConfiguration:
- @EnableConfigurationProperties({RedisProperties.class})
我们发现,RedisProperties开启了配置信息的自动绑定功能。我打开RedisProperties
我们发现,只要是以spring.redis开头的配置信息都会自动绑定到RedisProperties上。
- @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
LettuceConnectionConfiguration:
JedisConnectionConfiguration:
我们发现,RedisAutoConfiguration给我们提供了两个连接工厂。那我们默认使用的连接工厂是哪一个呢?我们测试一下:
@Autowired
RedisConnectionFactory connectionFactory;
@Test
public void test2(){
Class<? extends RedisConnectionFactory> aClass = connectionFactory.getClass();
System.out.println(aClass);
}
控制台输出:
class org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
如果我们就想使用JedisConnectionFactory呢?我们需要在配置文件指定使用JedisConnectionFactory:
spring:
redis:
host: 192.168.10.150
port: 6379
client-type: jedis
此外还需要添加一个jedis的依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
再次测试:
@Autowired
RedisConnectionFactory connectionFactory;
@Test
public void test2(){
Class<? extends RedisConnectionFactory> aClass = connectionFactory.getClass();
System.out.println(aClass);
}
控制台输出:
class org.springframework.data.redis.connection**.jedis.JedisConnectionFactory**
- RedisTemplate / StringRedisTemplate
我们发现:RedisTemplate和StringRedisTemplate这两个都是操作redis的两个客户端。
然后我们在application.yml配置文件中对redis进行相关配置:
spring:
redis:
host: 192.168.10.150
port: 6379
测试
@SpringBootTest(classes = App.class)
public class TestRedis {
@Autowired
StringRedisTemplate redisTemplate;
@Test
public void test(){
redisTemplate.opsForValue().set("username","eric");
}
}
2.5 springboot整合junit
2.5.1 junit5简介
Junit5的框架主要有三个部分组成分别是:Junit Platform + Junit Jupiter + Junit Vintage3
-
Junit Platform :
其主要作用是在 JVM 上启动测试框架。它定义了一个抽象的 TestEngine API 来定义运行在平台上的测试框架;也就是说其他的自动化测试引擎或开发人员⾃⼰定制的引擎都可以接入 Junit 实现对接和执行。同时还支持通过命令行、Gradle 和 Maven 来运行平台(这对于我们做自动化测试至关重要) -
Junit Jupiter:
这是 Junit5 的核心,可以看作是承载 Junit4 原有功能的演进,包含了 JUnit 5 最新的编程模型和扩展机制;很多丰富的新特性使 JUnit ⾃动化测试更加方便、功能更加丰富和强大。也是测试需要重点学习的地方;Jupiter 本身也是⼀一个基于 Junit Platform 的引擎实现,对 JUnit 5 而言,JUnit Jupiter API 只是另一个 API!。 -
Junit Vintage3
Junit 发展了10数年,Junit 3 和 Junit 4 都积累了大量的⽤用户,作为新一代框 架,这个模块是对 JUnit3,JUnit4 版本兼容的测试引擎,使旧版本 junit 的⾃动化测试脚本也可以顺畅运行在 Junit5 下,它也可以看作是基于 Junit Platform 实现的引擎范例。
注意:
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
我们现在要在springboot里面使用junit5.我们直接在pom文件里面引入相关的启动器即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest(classes = App.class)
public class TestRedis {
@Test
public void test(){
}
}
2.5.2 junit5常用测试注解
- **@DisplayName 😗*为测试类或者测试方法设置展示名称
@DisplayName("junit5单元测试")
@SpringBootTest(classes = App.class)
public class TestJunit5 {
@DisplayName("test01方法")
@Test
public void test01(){
System.out.println("这是test01方法");
}
}
- **@BeforeEach 😗*表示在每个单元测试之前执行
@DisplayName("junit5单元测试")
@SpringBootTest(classes = App.class)
public class TestJunit5 {
@BeforeEach
public void beforeEach(){
System.out.println("这是beforeEach方法");
}
@DisplayName("test01方法")
@Test
public void test01(){
System.out.println("这是test01方法");
}
}
控制台打印输出:
这是beforeEach方法
这是test01方法
- **@AfterEach 😗*表示在每个单元测试之后执行
@AfterEach
public void afterEach(){
System.out.println("这是afterEach方法");
}
执行test01方法,控制台打印输出:
这是beforeEach方法
这是test01方法
这是afterEach方法
- **@BeforeAll 😗*表示在所有单元测试之前执行
@BeforeAll
public static void beforeAll(){ //注意这个方法需要使用static关键字修饰
System.out.println("这是beforeAll方法");
}
运行整个单元测试类,让所有测试方法都运行,观察控制台运行效果:
这是beforeAll方法
这是beforeEach方法
这是test01方法
这是afterEach方法
这是beforeEach方法
这是test02方法
这是afterEach方法
@AfterAll
public static void afterAll(){ //注意这个方法需要使用static关键字修饰
System.out.println("afterAll方法执行了");
}
运行整个单元测试类,让所有测试方法都运行,观察控制台运行效果:
这是beforeAll方法
这是beforeEach方法
这是test01方法
这是afterEach方法
这是beforeEach方法
这是test02方法
这是afterEach方法
afterAll方法执行了
- **@Disabled 😗*表示测试类或测试方法不执行
@Disabled //这个测试方法不执行
@Test
public void test03(){
System.out.println("这是test03方法");
}
- **@Timeout 😗*表示测试方法运行如果超过了指定时间将会返回错误
@DisplayName("test02方法")
@Timeout(value = 500,unit = TimeUnit.MILLISECONDS)
@Test
public void test02(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("这是test02方法");
}
2.5.3 springboot中的断言机制
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:
检查业务逻辑返回的数据是否合理。
所有的测试运行结束以后,会有一个详细的测试报告
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
- assertEquals 判断预期值和实际值是否相等,或者两个对象是否是同一个对象
@SpringBootTest(classes = App.class)
public class TestAssert {
@Test
public void test01(){
/**
* assertEquals 判断两个值是否相等
* 参数1: 预期值
* 参数2:实际值
* 参数3:断言失败,输出的提示信息
*/
Assertions.assertEquals(4,getNum(2,3),"断言失败,预期值和实际值不符合");
System.out.println("test01方法执行了");
}
public int getNum(int num1,int num2){
return num1 + num2;
}
}
控制台输出效果:
也可以断言两个对象的内存地址是否一致
@Test
public void test01(){
/**
* assertEquals 判断两个值是否相等
* 参数1: 预期值
* 参数2:实际值
* 参数3:断言失败,输出的提示信息
*/
Object o1 = new Object();
Object o2 = new Object();
Assertions.assertEquals(o1,o2,"两个对象不是同一个对象");
System.out.println("test01方法执行了");
}
- assertSame 判断两个对象是否是同一个对象
@Test
public void test02(){
Object o1 = new Object();
Object o2 = new Object();
//Assertions.assertSame(o1,o2,"两个对象不是同一个对象");
Assertions.assertNotSame(o1,o2,"两个对象同一个对象");
System.out.println("test02方法执行了");
}
- assertFalse 判断结果是否为false
- assertTrue 判断结果是否为true
@Test
public void test03(){
//Assertions.assertTrue(2 > 1);
Assertions.assertFalse(2 > 1,"结果为true");
System.out.println("test03方法执行了");
}
- 数组断言
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等。
@Test
public void test04(){
//Assertions.assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
Assertions.assertArrayEquals(new int[]{1, 2}, new int[] {1, 2,3},"数组内容不相等");
System.out.println("test04方法执行了");
}
控制台输出结果:
- 组合断言
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。
@Test
public void test05(){
Assertions.assertAll("Math",
() -> Assertions.assertEquals(2, 1 + 1,"预期值与实际值结果不符"),
() -> Assertions.assertTrue(1 > 8,"预期结果不为true")
);
System.out.println("test05方法执行了");
}
控制台输出:
- 异常断言
JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。
@Test
public void test06(){
Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0),"怎么没有出异常?");
System.out.println("test06方法执行了");
}
- 超时断言
Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间
@Test
@DisplayName("超时测试")
public void test07() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(1500),"执行超时");
System.out.println("test07方法执行了");
}
控制台输出:
断言还会给我们生成测试报告,当我们执行test命令之后,我们可以在控制台看到具体的测试报告
2.5.4 单元测试-- 前置条件
JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
@Test
public void test08(){
Assumptions.assumeTrue(false,"预期结果不为true");
System.out.println("test08方法执行了");
}
控制台打印输出:
当我们执行test。我们发现test08方法根本就不会被执行。直接被忽略。但是不满足断言条件的方法还是会被执行。
2.5.5 单元测试–参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
- @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型。
- @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)。
@SpringBootTest(classes = App.class)
public class TestParameter {
@ParameterizedTest //参数化测试的注解
@ValueSource(strings = {"apple", "orange", "banana"})
@DisplayName("基于值的参数化测试")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("show") //指定方法名
@DisplayName("基于方法的参数化测试")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> show() {
return Stream.of("apple", "banana");
}
}
2.6 springboot指标监控
2.6.1 Actuator与endpoint
在生产环境中,每一个微服务在部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
- 引入actuator相关的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 对actuator监控的端点进行暴露
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露
那么Actuator会对哪些断点进行监控呢,我们可以看springboot看官网描述:
https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
我们启动springboot项目,就可以去查看被actuator监控的相关指标信息。
- http://localhost:8080/actuator/beans 监控当前web应用中被容器管理的bean有哪些:
我们通过postman发送请求,就可以看到actuator监控的到的被容器管理的bean的详细信息:
- http://localhost:8080/actuator/configprops 监控当前web应用中的配置信息:
- http://localhost:8080/actuator/metrics 监控当前web应用中的系统指标信息:
没有获取到对应指标的值,我们可以在url后面继续拼接上面的指标名称即可获取。
http://localhost:8080/actuator/metrics/jvm.buffer.memory.used
2.6.2 endpoint的开启和禁用
2.6.2.1 health端点
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
- health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
- 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
我们现在发送请求:http://localhost:8080/actuator/health
但是我们看不到我们应用不健康的具体信息,我们只能通过应用的控制台去查看,这样很不方便。
我们可以对指定端点进行详细配置,这样我们可以在发送请求的时候,查看服务器健康与否的具体信息。
如果我们把redis服务开启:
2.6.2.2 端点的禁用和开启
如果我们想禁用对某个断点的监控,我们需要进行相关的配置:
management:
endpoint:
beans: #禁用对beans这个断点的监控
enabled: true
或者禁用所有的Endpoint然后手动开启指定的Endpoint
management:
endpoints:
enabled-by-default: false #禁用所有断点的监控
endpoint:
beans: #开启指定端点的监控
enabled: true
health:
enabled: true
2.6.3 Admin Server可视化界面监控
(1) 搭建可视化监控后台项目
- 引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
- 定义启动器
@EnableAdminServer //开启性能指标监控
@SpringBootApplication
public class AdminServerApplication {
public static void main(String[] args) {
SpringApplication.run(AdminServerApplication.class, args);
}
}
- 定义配置文件
server.port=8888
项目最终目录结构如下:
(2) 配置微服务
在我们需要监控的微服务的配置文件中加入相关的依赖:
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.3.1</version>
</dependency>
在我们需要监控的微服务的配置文件中加入如下配置:
(3) 访问后台可视化界面:
http://localhost:8888/
2.7 springboot高级特性
2.7.1 profile环境切换
2.7.1.1 配置文件环境的切换
在实际的工作中,我们会分为开发环境,测试环境,生产环境。不同的环境,我们项目的配置文件是不一样的,比如我们数据库的配置信息。我们在实际工作中,我们应该如何切换我们的环境呢?
现在我们创建一个新项目,来演示springboot中多环境的切换。
- 引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 编写启动器
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
- 编写控制器方法
需求:动态获取不同环境配置文件中的name值。并将值响应在浏览器页面上。
@RestController
public class HelloController {
@Value("${person.name:李四}") //如果没有获取到,就取默认值李四
public String name;
@RequestMapping("hello")
public String sayHello(){
return "hello," + name;
}
}
- 编写配置文件
默认配置文件 application.properties:
server.port=8080
指定环境配置文件 application-{env}.yml
比如开发环境配置文件: dev application-dev.yml
生产环境配置文件:prod application-prod.yml
测试环境配置文件: test application-test.yml
现在我们模拟定义生产环境配置文件( application-prod.yml):
person:
name: prod-张三
server:
port: 8000
测试环境配置文件(application-test.yml):
person:
name: test-张三
server:
port: 7000
最后项目定义目录如下:
现在我们启动项目:
我们发现,加载的配置文件是默认的配置文件信息,也就是application.properties。
现在我们想获取指定配置文件上的name值,应该如何去做?
方式1:在配置文件中去指定
在springboot默认配置文件中,也就是在application.properties中定义相关的配置信息:
server.port=8080
# 指定加载prod环境中的配置信息
spring.profiles.active=prod
我们再次启动项目:
我们发送请求:
方式2:jar包运行的时候,指定配置环境
执行install命令,将项目打成jar包。
执行命令:
java -jar springboot-profile-1.0-SNAPSHOT.jar --spring.profiles.active=test
我们发现,现在启动的端口是测试配置文件定义的端口。
http://localhost:7000/hello
2.7.1.2 @Profile注解
- 定义接口
public interface Person {
String getName();
Integer getAge();
}
- 定义pojo
@Profile("test") 指定加载test环境的配置文件
@Component
@ConfigurationProperties("person")
@Data
public class Worker implements Person {
private String name;
private Integer age;
}
@Profile(value = {"prod"}) //指定加载prod环境的配置文件
@Component
@ConfigurationProperties("person")
@Data
public class Boss implements Person {
private String name;
private Integer age;
}
- 定义controller
@RestController
public class HelloController {
@Autowired
Person person;
@RequestMapping("person")
public String sayPerson(){
return person.getClass().toString();
}
}
- 定义配置文件
application.properties
server.port=8080
#指定加载prod环境的配置文件
spring.profiles.active=prod
application-prod.properties
person:
name: prod-张三
age: 12
server:
port: 8000
application-test.properties
person:
name: test-张三
age: 20
server:
port: 7000
启动发送请求:
http://localhost:8000/person
2.7.1.3 profile分组
我们也可以将配置文件分组,多个配置文件一同加载。
application.properties
server.port=8080
# myprod组名 自动加载myprod组的配置文件
spring.profiles.active=myprod
# 加载myprod中的application-prod.yaml文件
spring.profiles.group.myprod[0]=prod
# 加载myprod中的application-dev.yaml文件
spring.profiles.group.myprod[1]=dev
application-prod.properties
person:
name: prod-张三
server:
port: 8000
application-dev.properties
person:
age: 20
定义controller
@RestController
public class HelloController {
@Autowired
Person person;
@RequestMapping("person1")
public Person getPerson(){
return person;
}
}
http://localhost:8000/person1
2.7.2 springboot配置文件加载优先级
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文 件
-
file:./config/ 项目根目录下面的config文件夹 优先级最高
-
file:./ 项目根目录下面
-
classpath:/config/ resources文件夹下面的config文件夹
-
classpath:/ resources文件夹下面 优先级最低
优先级由高到底,高优先级的配置会覆盖低优先级的配置;
SpringBoot会从这四个位置全部加载主配置文件;互补配置;
测试: 在以上各级文件夹下面定义application.properties文件,里面定义server.port端口号。看看到底以哪一个端口号为准。
小细节: 如果需要给访问路径加上虚拟目录,那么在配置文件中可以配置:
server:
port: 8081
servlet:
context-path: /springboot #指定虚拟路径
2.7.3 springboot自定义starter
我们知道,在springboot项目中,如果我们想使用哪个场景的技术,我们只需要导入对应场景的启动器就可以了。那么如果我们想自定义启动器给别人用,那应该怎么办呢?下面我们就自定义场景启动器。
第一步:创建一个空工程
首先我们创建一个空工程,因为我们需要在空工程里面放入很多子模块。
点击Next:
点击Finish。
接下来我们添加一个模块:
我们使用maven构建模块,这个模块就是定义我们场景启动器。
点击Finish:
然后我们再添加一个模块,这个模块的主要作用就是开启对应启动器的自动配置:
点击finsh.最后创建目录结构如下:
第二步:导入依赖
首先在xq-hello-spring-boot-starter-autoconfigure中导入依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
以后我们只需要把xq-hello-spring-boot-starter启动器给别人引用就可以了。但是所有代码逻辑都是在xq-hello-spring-boot-starter-autoconfigure里面编写的。
所以我们需要在xq-hello-spring-boot-starter里面引入xq-hello-spring-boot-starter-autoconfigure相关的依赖。
<dependencies>
<dependency>
<groupId>com.xq</groupId>
<artifactId>xq-hello-spring-boot-starter-autoconfigure</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
第三步: 编写业务代码
接下来我们需要在xq-hello-spring-boot-starter-autoconfigure中定义相关的业务代码。
- 编写pojo
@ConfigurationProperties("xq.hello")
public class HelloProperties {
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
也就是我们以后定义xq.hello开头的配置信息都会自动的被绑定到HelloProperties类所属的bean中。
- 编写业务代码
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayHello(String userName){
return helloProperties.getPrefix() + "<-->"+userName+"<-->"+helloProperties.getSuffix();
}
}
注意:这个业务类不要直接使用@Component注解管理,因为用户要使用这个业务类。是需要开启自动配置,这个bean才会被放在ioc容器里面去的
- 编写自动配置类
开启对业务代码的自动配置。一般这个类我们都名为为XxxAutoConfiguration
@Configuration
@EnableConfigurationProperties(HelloProperties.class) //默认HelloProperties放在容器中
public class HelloServiceAutoConfiguration{
//容器中没有这个bean,我们在注入到容器里面去
@ConditionalOnMissingBean(HelloService.class)
@Bean
public HelloService helloService(){
HelloService helloService = new HelloService();
return helloService;
}
}
最后定义目录结构如下:
第四步:编写spring.factories配置文件
springboot项目启动之后,启动器里面的资源要生效,都是通过加载spring.factories配置文件才会生效(我们之前在分析springboot自动配置原理的时候说过)。
所以我们也需要定义一个名为spring-factories配置文件。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xq.autoConfigure.HelloServiceAutoConfiguration
最后我们将xq-hello-spring-boot-starter和xq-hello-spring-boot-starter-autoconfigure分别clean,再intall。将打成的jar包发布到本地仓库即可。
第五步:使用自定义的starter
我们打开一个新项目,引入我们自定义的启动器依赖。
- 引入依赖
<dependency>
<groupId>com.xq</groupId>
<artifactId>xq-hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 编写controller
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@RequestMapping("hello")
public String sayHello(){
String message = helloService.sayHello("eric");
return message;
}
}
- 编写配置文件
xq:
hello:
prefix: hello
suffix: springboot
- 测试
http://localhost:8080/hello
到这里,我们自定义启动器的过程也就完成了。
更多推荐
所有评论(0)