Springboot(1)
一.介绍
1.什么是Springboot
- 随着 Spring不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。SpringBoot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;
- Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。**Spring Boot以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring配置。**同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),SpringBoot 应用中这些第三方库几乎可以零配置的开箱即用。
- 简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,springboot整合了所有的框架 。
2.SpringBoot优点
- 创建独立Spring应用
- 内嵌web服务器
- 自动starter依赖,简化构建配置
- 自动配置Spring以及第三方功能
- 提供生产级别的监控、健康检查及外部化配置
- 无代码生成、无需编写XML
3.微服务架构
- 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 每个服务运行在自己的进程内,也就是可独立部署和升级
- 服务之间使用轻量级HTTP交互
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
二.入门
1.项目创建方式一:使用Spring Initializr 的 Web页面创建项目
- 打开 https://start.spring.io/
- 填写项目信息
- 点击”Generate Project“按钮生成项目;下载此项目
- 解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。
- 如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
2.项目创建方式二:使用 IDEA 直接创建项目(推荐)
项目结构分析:
通过上面步骤完成了基础项目的创建。就会自动生成以下文件:
1、程序的主启动类
2、一个 application.properties 配置文件
3、一个 测试类
4、一个 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 父依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.liqingfeng</groupId>
<artifactId>springboot-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-01</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- web场景启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springboot单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
现在我们写一个controller业务
1、在主启动类的同级目录下新建一个controller包(必须是同级目录下,底层写好的),并新建一个HelloController类
2、从主程序类中启动项目(执行main方法),请求hello请求,页面显示返回的数据,OK
将项目打成jar包,发布出去
1.打包之前,在pom.xml中配置打包时跳过test测试(节省打包时间,且jar包也不会太大)
<!--在工作中,很多情况下我们打包是不想执行测试用例的(测试影响数据库数据),所以跳过测试-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!--跳过项目运行测试用例-->
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
2.执行打包操作
3.打包成功会在target目录下看到对应jar包
4.将该jar包拖到桌面并执行cmd的java -jar命令执行jar包
5.浏览器请求url,访问OK
3.自定义banner启动图标
在resources目录下新建一个banner.txt,其内容可在https://www.bootschool.net/ascii
生成
三.自动配置原理(重点)
我们之前写的springboot01 web项目,执行主启动类main方法就能运行服务。到底是怎么运行的呢?
1.从pom.xml文件探究起
点进去spring-boot-starter-parent
,发现还有一个父依赖spring-boot-dependencies
这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;
以后我们导入依赖默认是不需要写版本;但是如果导入的包没有在依赖中管理着就需要手动配置版本了
我们pom.xml要开发web应用,如下配置了,就不用写版本号,因为依赖中版本已控制好,而且spring-boot-starter-web叫web场景启动器,该启动器会导入web开发中正常运行所依赖的jar包(其中包含嵌入的tomcat服务器)
2.启动器
以后我们看到的springboot-boot-starter-xxx
,就是spring-boot的各场景启动器
看到:xxx-spring-boot-starter
就是第三方为我们提供的,简化开发的场景启动器
官网启动器详解:https://docs.spring.io/spring-boot/docs/2.4.11/reference/html/using-spring-boot.html#using-boot-starter
SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器),只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来 , 我们要用什么功能就导入什么样的场景启动器即可,我们未来也可以自己自定义 starter
3.主启动类
@SpringBootApplication
public class Springboot01Application {
public static void main(String[] args) {
SpringApplication.run(Springboot01Application.class, args);
}
}
@SpringBootApplication注解
作用:标注在某个类上说明这个类是SpringBoot的主配置类 , SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;
进入这个注解:可以看到上面还有很多其他注解!
@SpringBootApplication
等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("......")
1.@ComponentScan
作用:自动扫描并加载符合条件的bean组件到IOC容器中,spring中有讲解
2.@SpringBootConfiguration
我们继续进去这个@SpringBootConfiguration注解查看,该注解又是一个@Configuration
点@Configuration注解查看,@Configuration注解又是一个 @Component
3.@Configuration
功能:作用的类是一个配置类(配置类就是对应Spring的xml 配置文件)
- Full模式(@Configuration(proxyBeanMethods = true)),是默认模式
- Lite模式(@Configuration(proxyBeanMethods = false))
两种模式推荐最佳实战
- 配置类的bean组件之间有依赖关系用Full模式(每个@Bean方法被调用多少次返回的bean组件都是单实例的(配置类里面使用@Bean标注在方法上给容器注册组件默认是单实例的),也就是这些bean组件地址是同一个,因为会去spring容器中找这个bean组件)
- 配置类的bean组件之间无依赖关系用Lite模式(每个@Bean方法被调用多少次返回的bean组件都是新创建的,也就是这些bean组件地址不是同一个,因为会直接new一个bean组件返回,不会去spring容器中找。不过,可以加速容器启动过程、减少判断)
4.@Component
功能:作用的类,该类是spring容器里的bean组件。
所以说,主启动类本身也是Spring中的一个配置类也是一个bean组件,负责启动应用!
5.@EnableAutoConfiguration(重要的注解)
功能:开启自动配置功能
6.@AutoConfigurationPackage
@import : 给spring容器中导入Registrar bean组件
Registrar类作用:将主启动类的所在包及包下面所有子包里面的所有bean组件注册到Spring容器
下图通过debug追踪AutoConfigurationPackage.class.getName()就是扫描主启动类(Springboot01Application)包下的所有组件
7.@Import
- @Import( ):给spring容器导入指定的bean组件
- @Import(AutoConfigurationImportSelector.class):给spring容器注册AutoConfigurationImportSelector bean组件,而AutoConfigurationImportSelector是自动配置导入选择器,它被spring容器托管,那么它会导入哪些自动配置类呢?
1、AutoConfigurationImportSelector类中有一个这样的方法
// 获得候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//这里的getSpringFactoriesLoaderFactoryClass()方法
//返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
2、这个方法又调用了 SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
//这里它又调用了 loadSpringFactories 方法
return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
3、我们继续点击查看 loadSpringFactories 方法
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
//获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
Map<String, String> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
//去获取一个资源 "META-INF/spring.factories"
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
//将读取到的资源遍历,封装成为一个Properties
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
......
}
4、spring.factories
我们根据源头打开spring.factories ,看到了很多自动配置类的声明,这就是自动配置根源所在!
debug看自动配置类注册到容器的情况如下图
虽然我们130个场景的所有自动配置类(xxxxAutoConfiguration)启动的时候默认全部加载,但由于每个自动配置类@Conditional注解,所以只有满足条件的自动配置类才会最终配置成功
我们在spring.factories自动配置类声明文件中随便找一个自动配置类,比如WebMvcAutoConfiguration
可以看到这些一个个的都是配置类,而且都注入了一些Bean
所以,自动配置的真正实现是从classpath路径下搜寻所有的META-INF/spring.factories配置文件 ,并将其中所有的 org.springframework.boot.autoconfigure…xxxAutoConfiguration自动配置类导入到spring容器(其中自动配置类中的被@bean注解的也会导入到spring容器)
总结:
- SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration下的所有值
- 通过这些值找到对应的130个自动配置类并加载 ,按条件生效某些自动配置类,注册到spring容器
- 整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
- 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 ,并配置好这些组件 ;
- 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;
8.@Conditional
@Conditional:条件装配注解,被注解的类或方法只有满足@Conditional指定的条件才能进行该类或方法的操作(比如bean组件注入、程序执行等)
下面@ConditionalOnxxx都是基于xxx条件成立时被注解的类或方法才生效
比如@ConditionalOnBean(View.class):在spring容器中存在View类型的bean组件,才能使类或方法生效
9.SpringApplication类
SpringApplication.run( ) 分析(最初以为就是运行了一个main方法,没想到却开启了一个web服务)
- 一是SpringApplication的实例化
- 二是run方法的执行;
SpringApplication类主要做了以下四件事情:
- 推断应用的类型是普通的项目还是Web项目
- 查找并加载所有可用初始化器, 设置到initializers属性中
- 找出所有的应用程序监听器,设置到listeners属性中
- 推断并设置main方法的定义类,找到运行的主类
- 创建spring IOC容器(ApplicationContext)并创建容器中的所有组件
- 最后执行run方法开启服务
run方法流程分析
4. 自动配置原理总结(对于导入web启动器而言)
- 自动配置好了Tomcat(包括引入Tomcat依赖、 配置Tomcat)
- 自动配置好了SpringMVC(包括引入SpringMVC全套组件、自动配好SpringMVC常用的一些bean组件功能,如:字符编码问题)
- 默认的包结构(包括主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来,无需以前的包扫描配置)
- 各种配置拥有默认值(包括对应的xxxProperties配置文件最终都是映射到某个自动配置类上,如:MultipartProperties映射到MultipartAutoConfiguration上,而springboot配置文件(application.properties或application.yml)与xxxProperties配置文件绑定的,所以我们在springboot配置文件上进行添加功能能生效,是因为数据最终传到了自动配置类的各种bean组件上)
- 按需加载所有自动配置项(包括由于有非常多的starter启动器,pom.xml中引入了哪些场景的启动器,这个场景的自动配置才会生效,springBoot所有的自动配置类功能都在spring.factories自动配置类声明文件里面
另一种总结:
- SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
- 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值(xxxxProperties里面拿)。xxxProperties又是和application.properties(yml)配置文件进行了绑定
- 生效的配置类就会给容器中装配很多bean组件
- 只要容器中有这些bean组件,相当于这些功能就有了
- 定制化配置:在application.properties(yml)配置文件中进行配置、或则@Bean替换底层的组件、用户去看这个组件是获取的配置文件什么值就去修改。
自动配置流程:加载所有的xxxxxAutoConfiguration自动配置类 —>@Conditional觉得哪些 导入哪些自动配置类到spring容器中—> 每个自动配置类都从xxxxProperties类里面拿值 ----> xxxxProperties类绑定到springboot的application.properties(yml)配置文件
5.springboot最佳开发实践
- 引入场景启动器
○ https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter - 查看自动配置了哪些(可以不用看)
○ 在application.properties(yml)配置文件中添加debug=true开启自动配置信息、可以在控制台看哪些bean组件Negative(不生效)\Positive(生效) - 是否需要修改某些配置以满足需求
○ 第一种方式:参照文档修改配置项:https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties,然后自己分析xxxxProperties、最后在application.properties(yml)配置文件中配置。
○ 第二种方式:自定义配置类,为了加入或者替换spring容器的bean组件,利用@Bean、Configuration、@Component等
四.配置文件绑定、注入
springboot有两种配置文件格式(Properties和yaml)
yaml语法结构 :key:空格value
Properties语法结构 :key=value
配置文件的作用 :设置application.properties(yml)配置文件就可以修改SpringBoot自动配置类中一些bean组件的默认值,因为SpringBoot在底层都给我们自动配置好了
比如我们可以在application.properties配置文件中修改Tomcat 默认启动的端口号!这样我们只能通过8081端口访问我们的服务。
1.yaml(或yml)配置文件
1.概述
- YAML是 “YAML Ain’t a MarkupLanguage”(YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“YetAnotherMarkupLanguage”(仍是一种标记语言)
- 这种语言以数据作为中心,而不是以标记语言为重点!
以前的配置文件,大多数都是使用xml来配置,但yam语法要求严格,具体有如下几点:
- 空格不能省略
- 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
- 属性和值的大小写都是十分敏感的。
- 字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;
注意:
字符串加了“ ” 双引号表示不会转义字符串里面的特殊字符 ,特殊字符会作为本身想表示的意思;
比如 :name: “kuang \n shen” 输出 :kuang 换行 shen
’ ’ 单引号会转义特殊字符 ,特殊字符最终会变成和普通字符一样输出
比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen
yaml的松散绑定:在yml中写的last-name,等价于在java类中写的lastName, - 后面跟着的字母默认是大写的。这就是松散绑定。
2.对象、Map(键值对)表示
k:
v1:
v2:
在下一行来写对象的属性和值得关系,注意缩进。比如:
student:
name: qinjiang
age: 3
行内写法(推荐)
student: {name: qinjiang,age: 3}
3.数组( List、set )表示
用 - 值表示数组中的一个元素,比如:
pets:
- cat
- dog
- pig
就表示pets = [cat,dog,pig]
行内写法(推荐)
pets: [cat,dog,pig]
4.普通值:单个的、不可再分的值。如date、boolean、string、number、null
k: v
5.yaml配置文件注入
配置文件(Properties和yaml)文件强大的地方在于可以给我们的java类进行值的注入!
yaml注入配置文件
1、在springboot项目中的resources目录下新建一个文件 application.yml
2、编写一个实体类 Dog;
@Component //注册bean到容器中
public class Dog {
private String name;
private Integer age;
//有参无参构造、get、set方法、toString()方法
}
3、思考,我们原来是如何给bean注入属性值的?@Component+@Value,测试一下:
@Component //注册bean
public class Dog {
@Value("阿黄")
private String name;
@Value("18")
private Integer age;
}
4、在SpringBoot的测试类下注入输出一下;
@SpringBootTest
class DemoApplicationTests {
@Autowired //按类型自动装配
Dog dog;
@Test
public void contextLoads() {
System.out.println(dog);
}
}
结果成功输出,@Value注入成功,这是我们原来的办法对吧。
5、编写一个复杂一点的实体类:Person 类
@Component //注册bean到容器中
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
//有参无参构造、get、set方法、toString()方法
}
6、使用yaml配置的方式进行注入,在application.yml配置文件中进行配置!
person:
name: qinjiang
age: 3
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: 旺财
age: 1
7、我们刚才已经把person这个对象的所有值都写好了,我们现在来注入到我们的类中!(第一种配置绑定:@ConfigurationProperties+@Component)
/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个Person bean组件中;告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person”: 将配置文件中的person下面的所有属性一一对应
*/
@Component //注册bean,只有在spring容器中的组件才会拥有SpringBoot提供的强大功能
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
}
第二种配置绑定:@EnableConfigurationProperties + @ConfigurationProperties
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(Person.class)
//@EnableConfigurationProperties(Person.class):作用1、开启Person类的配置绑定功能 2、把Person类作为bean组件注册spring到容器中
public class MyConfig {
}
使用@ConfigurationProperties(prefix = “person”)注解时IDEA 爆红如下图(springboot配置注解处理器没有找到),不过这个爆红不影响,所以依赖可以不用导入
让我们看springboot官方文档,需要一个依赖,导入即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
8、在springboot自带的测试类中测试
@SpringBootTest
class Springboot01ApplicationTests {
@Autowired
Person person;
@Test
void contextLoads() {
System.out.println(person);
}
}
结果:所有值全部注入成功!
Person{name='liqingfeng', age=3, happy=true, birth=Wed Aug 25 00:00:00 CST 1999, maps={k1=v1, k2=v2}, lists=[code, girl, music], dog=Dog{name='旺财', age=1}}
6.配置文件占位符
配置文件还可以编写占位符生成随机数
person:
name: qinjiang${random.uuid} # 随机uuid
age: ${random.int} # 随机int
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: ${person.hello:other}_旺财
age: 1
2.加载指定的配置文件(了解)
- @PropertySource :加载指定的配置文件
- @configurationProperties:默认从application.properties(yml)配置文件中获取值
1、我们在resources目录下新建一个cat.properties文件
name=liqingfeng
2、然后在我们的代码中指定加载cat.properties文件
@Component
@PropertySource("classpath:cat.properties")
public class Cat {
@Value("${name}")
private String name;
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
3、再次输出测试一下:指定配置文件绑定成功!
@SpringBootTest
class Springboot01ApplicationTests {
@Autowired
Cat cat;
@Test
void contextLoads() {
System.out.println(cat);
}
}
Cat{name='liqingfeng'}
3properties配置文件
我们上面采用的yaml方法都是最简单的方式,开发中最常用的;也是springboot所推荐的!配置文件除了yml还有我们之前常用的properties
注意:properties配置文件在写中文的时候,会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8;
测试步骤:
1、在application.properties配置文件中写如下配置
person.name=liqingfeng
person.age=3
person.happy=true
person.birth=1999/08/25
#person.maps=k1:v1, k2:v2
person.lists=code, girl, musica
person.dog.name=小黄
person.dog.age=3
2、Person类使用@ConfigurationProperties绑定了配置文件application.properties
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
//get、set、tostring、有参无参
3、测试
@SpringBootTest
class Springboot01ApplicationTests {
@Autowired
Person person;
@Test
void contextLoads() {
System.out.println(person);
}
}
结果:(测试时发现Map类型不知道怎么赋值,这是Properties弱于yaml配置文件的体现)
Person{name='liqingfeng', age=3, happy=true, birth=Wed Aug 25 00:00:00 CST 1999, maps=null, lists=[code, girl, musica], dog=Dog{name='小黄', age=3}}
结论:
- application.properties配置文件或application.yml配置文件都可以获取到值 ,强烈推荐 yml
- 如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
- 如果说,我们专门编写了一个Java类和application.properties(yml)配置文件进行一 一映射,就直接使用application.yml
五、多环境切换
profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同环境的版本,实现快速切换开发环境
多配置文件
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本;
例如:
- application-test.properties(yml) 代表测试环境的配置文件
- application-dev.properties(yml) 代表开发环境的配置文件
1.使用application.properties的实现多环境开发选择(推荐)
但是Springboot并不会直接启动这些配置文件,默认使用application.properties(yml)配置文件。我们需要在application.properties(yml)配置文件里通过spring.profiles.active=profile
来选择需要激活并使用的环境
2.使用application.yml的实现多环境开发选择(不推荐)
application.yml和application.properties配置文件实现效果一样,但是使用yml去实现不需要创建多个配置文件,更加方便了 !
server:
port: 8080
spring:
profiles:
active: dev
--- #分隔符
server:
port: 8081
spring:
profiles: test #配置文件名称
--- #分隔符
server:
port: 8082
spring:
profiles: dev #配置文件名称
3.application.properties(yml)配置文件 启动时可扫描的位置(了解即可)
springboot 启动会扫描以下位置的application.properties(ym)文件作为Spring boot的配置文件:
- 优先级最高:项目路径下的config文件夹配置文件(对应1)
- 优先级次之:项目路径下配置文件(对应2)
- 优先级较低:classpath下的config文件夹配置文件(对应3)
- 优先级最低:classpath下配置文件(对应4)
高优先级的配置会覆盖低优先级的配置。SpringBoot会从这四个位置全部加载配置文件
4.修改项目的根访问路径
在application.properties(ym)配置文件中修改项目的根访问路径
server.servlet.context-path=/MrL
5.运维层面的配置文件位置指定
在服务器控制台上用命令方式指定配置文件的位置(通过spring.config.location来改变默认的配置文件位置)
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高
命令行窗口:
java -jar 项目名.jar --spring.config.location=F:/application.properties
六.SpringMVC自动配置原理(重点)
在进行web项目开发前,我们还需要知道SpringBoot对SpringMVC还做了哪些配置(包括如何扩展,如何定制)。只有把这些搞清楚了,我们在之后的开发中才会更加得心应手。
官方文档 :https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration
1.静态资源
1.静态资源默认存放目录、默认访问路径
- “classpath:/META-INF/resources/”
- “classpath:/resources/”
- “classpath:/static/”
- “classpath:/public/”
以上四个目录存放的静态资源可以被springboot容器识别并使用、默认url访问资源路径为:/**
(/**
表示当前项目根路径下的所有请求,现在我们的项目根路径是:/MrL
)
测试一下:
url访问地址:(均可以访问到静态资源)
http://localhost:8080/MrL/1.png
http://localhost:8080/MrL/2.png
http://localhost:8080/MrL/3.png
http://localhost:8080/MrL/4.png
如果controller处理的请求和需要访问的资源请求路径一样,则会执行controller处理的请求方法,不会找去找资源(即现去找先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器(/**
),静态资源也找不到则响应404页面
验证一下:(发现确实不会去找静态资源,而是走的controller)
springboot的默认静态资源存放目录和默认访问路径已经能满足我们的大部分web开发需求,一般就按照默认的来,自定义的话就修改访问路径行了。
2.自定义静态资源存放目录、访问路径
1.在application.properties配置文件中改变静态资源存放目录: (了解)
#静态资源存放目录:当前目录下的静态资源可以访问
spring.web.resources.static-locations=classpath:/aaa/
以下url访问静态资源OK:
http://localhost:8080/MrL/5.png
但是以下url不能访问静态资源:
http://localhost:8080/MrL/1.png
http://localhost:8080/MrL/2.png
http://localhost:8080/MrL/3.png
http://localhost:8080/MrL/4.png
所以,一旦定义了静态资源存放目录,原来的自动配置的默认值就失效了!
2.在application.properties配置文件中改变静态资源访问路径:
#静态资源访问路径:当前项目根路径的resources下的所有请求
spring.mvc.static-path-pattern=/resources/**
以下url访问静态资源OK:
http://localhost:8080/MrL/resources/1.png
3.webjar(了解)
Webjar本质就是以jar包的方式引入我们的静态资源 , 我们要导入一个静态资源文件,直接pom依赖导入即可,然后通过/webjars/**
来进行静态资源访问
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
下面的url是项目根路径为:/
我们来看看为什么通过/webjars/**
就可以访问静态资源:
原来WebMvcAutoConfiguration自动配置类帮我门自动映射了/webjars/**
,只要以/webjars/**
访问,若controller没有对应请求处理,则会在所有满足classpath:/META-INF/resources/webjars/
下去找webjar静态资源。所以前面的 /**
也是自动映射访问静态资源到classpath的四个目录下,原理我们接下来讲叙
2.静态资源原理(了解)
- SpringBoot启动默认加载130个xxxAutoConfiguration类(自动配置类)
- 由于我们开启web场景启动器,所以SpringMVC依赖被导入进来,自然WebMvcAutoConfiguration自动配置类就被注册到spring容器中
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {...}
找到WebMvcAutoConfigurationAdapter内部类
WebMvcAutoConfigurationAdapter只有一个有参构造,所以其参数的值会从spring容器中找到对应的bean组件并赋值
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class,
org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {...}
可以看到WebMvcProperties、WebProperties、ResourceProperties绑定了配置文件,所以我可以在配置文件中使用spring.mvc.xxx/spring.web.xxx来修改默认的配置
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {...}
@ConfigurationProperties("spring.web")
public class WebProperties {...}
WebProperties类中找到了静态资源默认存放目录有4个,和之前对应了。所以一旦我们在配置文件中进行spring.web.resources.static-locations=classpath:/aaa/修改,就会改变这里的staticLocations 变量,导致底层将采用我们的静态资源存放目录
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
3.欢迎页(首页)
在以下目录里若存在index.html
则会自动配置为欢迎页,默认访问路径为/**
- “classpath:/META-INF/resources/”
- “classpath:/resources/”
- “classpath:/static/”
- “classpath:/public/”
在4个目录中任选一个并新建一个 index.html
然后访问测试: http://localhost:8080/MrL/
- 如果自定义静态资源访问路径
/resources/**
,需要手动输入http://localhost:8080/MrL/resources/index.html
才能访问首页, - 如果通过
http://localhost:8080/MrL/
是访问不了首页的
当然访问首页除了springboot给的自动映射/**
以及自定义访问路径以外,还可以通过controller处理请求/
、/index.html
请求转发到index.html页面,不过这个需要模板引擎的支持,后面再说
4.欢迎页(首页)原理(了解)
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
通过debug计算表达式发现:
最终找到映射路径:/**
private String staticPathPattern = "/**";
这行代码是从4个静态资源默认存放路径遍历循环找到哪个目录有index.html
Resource resource = location.createRelative("index.html");
以下源码就是为什么静态资源访问路径一旦自定义,首页自动映射/**
就会失效,因为底层写死了首页访问必须是/**
,除非走controller
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
//要用欢迎页,欢迎页必须存在且静态资源访问路径就必须是:/**
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
//如果自定义了静态资源访问路径,就回去找controller,看哪个方法能处理欢迎页
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
5.图标(了解)
- 与其他静态资源一样,Spring Boot在静态资源默认存放路径中查找favicon.ico,如果存在这样的文件,它将自动作为网站的图标favicon
- 生成ico格式的工具网站:http://ico.chdyou.net/
springboot2.2以后就不再提供favicon.ico自动配置,WebMvcAutoConfiguration类里面也找不到相关的代码,因为图标一般都是前端工程的事情
所以如果想用,只能在想展示图标的页面head标签中加入以下标签(favicon.ico可以放在4个静态资源存放路径的任一 一个)
<link rel="icon" href="favicon.ico">
这里我是在index.html中加入图标的
页面标题左边就有自定义的图标了,不过只在添加了上述link标签的页面才有效
6. Rest及原理
Rest风格在springmvc中说过了,这里不再赘诉,就是URL相同的情况下,通过不同的请求方式来执行不同的controllrer
在springboot中如何使用Rest:
第一步:(在html页面中加一个name为_method
的input,value是请求方式,但是记得将表单method设置为POST。springboot底层会获取这个_method
参数的值)
<form action="/MrL/put" method="post">
<input type="hidden" name="_method" value="PUT">
<input type="text" name="用户名">
<input type="submit">
</form>
第二步:在配置文件里开启页面表单的Rest功能
spring.mvc.hiddenmethod.filter.enabled=true
编写controller,测试OK
@ResponseBody
@PutMapping("/put")
public String put(){
return "put方式的请求";
}
spitngboot的Rest原理(表单提交要使用REST的时候)
- 表单提交会带上_method=PUT
- 请求过来被HiddenHttpMethodFilter拦截
- 请求是否正常,并且是POST
- 获取到_method的值。
- 兼容以下请求:PUT.DELETE.PATCH
- 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
- 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
WebMvcAutoConfiguration自动配置类的OrderedHiddenHttpMethodFilter方法:开启Rest
HiddenHttpMethodFilter中写好了要获取请求方式的参数名为_method
,也可以通过自定义配置类将HiddenHttpMethodFilter注入spring容器,设置这个methodparam变量,使得请求方式的参数名为自定义的参数名)
下图说明表单提交method必须以POST为请求方式,否则springboot底层获取不到POST请求,也不能将该POST请求封装为请求参数_method
指定的请求方式的请求
7.请求参数及原理
1.有注解的参数
@PathVariable
(常用)、@RequestHeader
、@RequestAttribute
(获取请求域属性,和Request.getAttributef方法一样)、@RequestParam
(常用)、@MatrixVariable
(矩阵变量)、@CookieValue
、@RequestBody
写一个ParameterTestController 类,进行所有注解测试
@RestController
public class ParameterTestController {
// car/2/owner/zhangsan
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie){
Map<String,Object> map = new HashMap<>();
// map.put("id",id);
// map.put("name",name);
// map.put("pv",pv);
// map.put("userAgent",userAgent);
// map.put("headers",header);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
map.put("_ga",_ga);
System.out.println(cookie.getName()+"===>"+cookie.getValue());
return map;
}
@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}
//1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
//2、SpringBoot默认是禁用了矩阵变量的功能
// 手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
// removeSemicolonContent(移除分号内容)false支持矩阵变量
//3、矩阵变量必须有url路径变量才能被解析
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
Map<String,Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
map.put("path",path);
return map;
}
// /boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;
}
}
2.Servlet相关类型的参数
WebRequest
、ServletRequest
(常用)、ServletRsponse
(常用)、MultipartRequest
、 HttpSession
(常用)、javax.servlet.http.PushBuilder
、Principal
、InputStream
、Reader
、HttpMethod
、Locale
、TimeZone
、ZoneId
ServletRequestMethodArgumentResolver类可以对以上参数进行解析
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
3.Model等类型的参数
Map
、Model
(常用)(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult
、RedirectAttributes
( 重定向携带数据)、SessionStatus
、UriComponentsBuilder
、ServletUriComponentsBuilder
Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据,等同于request.getAttribute()
Map、Model类型的参数,会返回 mavContainer.getModel();—> BindingAwareModelMap 是Model 也是Map
mavContainer.getModel(); 获取到值的
4.自定义类型的参数
一般参数都是自定义java bean类型的参数,将请求中传来的值封装为请求参数中对应的对象
/** 前端表单
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
//自定义的java bean类
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
//get、set、有参无参、tosring
}
public class Pet {
private String name;
private String age;
//get、set、有参无参、tosring
}
//controller中参数类型为自定义的java bean类,前端发起请求到该controller接口,springmvc底层会自动封装为person类型,前提是前端的name必须与java bean的属性一样
@ResponseBody
@GetMapping("/hello")
public String hello(Person person) {
System.out.println(person);
}
5原理(了解)
所有的请求都会走DispatcherServlet这个请求转发Servlet,因为springboot web开发底层还是springmvc
所以每次请求都会执行DispatcherServlet的doDispatch( )方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx
在doDispatch打断点,调试看结果
其中RequestMappingHandlerMapping
类就保存了所有@RequestMapping
注解和handler
的映射规则,只要是@RequestMapping
注解的controller
都会被RequestMappingHandlerMapping
类处理
所有的请求处理器映射都在xxxHandlerMapping
中
- SpringBoot自动配置了欢迎页的 WelcomePageHandlerMapping ,访问
/**
能访问到index.html - SpringBoot自动配置了
RequestMappingHandlerMapping,被@RequestMapping
注解的controller
方法都能使用 - 请求进来,挨个尝试所有的
HandlerMapping
看是能处理请求信息
如果有就找到这个请求对应的HandlerMapping
如果没有就是下一个HandlerMapping
- 我们如果需要一些自定义的映射处理,我们也可以自己给容器中放
HandlerMapping
,自定义HandlerMapping
遍历循环每个handlerMapping找到能处理url请求的handlerMapping
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
doDispatch方法中这一行代码,就是执行controller方法
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
但在执行之前还确定了请求参数的类型,用什么注解的方法参数,一 一确定好才开始执行controller方法
即:
- HandlerMapping中找到能处理请求的Handler(Controller.method())
- 为当前Handler 找一个适配器 HandlerAdapter
- 适配器执行目标方法并确定方法参数的每一个值及类型
RequestMappingHandler:方法上标注@RequestMapping 都能适配这个适配器
HandlerFuncationAdapter:函数式controller方法都能适配这个适配器
真正执行handler目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
debug单步执行下去:
//执行目标方法
mav = invokeHandlerMethod(request, response, handlerMethod);
//ServletInvocableHandlerMethod
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
参数解析器-HandlerMethodArgumentResolver
- 确定将要执行的目标方法的每一个参数的值是什么
- SpringMVC目标方法能写多少种参数类型,取决于参数解析器
解析器工作过程:1.当前解析器是否支持解析这种参数supportsParameter
2.支持就调用resolveArgument
如何确定目标方法每一个参数的值
InvocableHandlerMethod类
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
......
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
......
}
挨个判断所有参数解析器哪个支持解析这个参数
解析这个参数的值:调用各自xxxMethodArgumentResolver
参数解析器的resolveArgument
方法
自定义的java bean类型是通过ServletModelAttributeMethodProcessor
参数解析器解析的
ServletModelAttributeMethodProcessor可以解析自定义的java bean,但是我们是由数据绑定器确定request请求中数据类型(string)转换为指定的类型(比如JavaBean中int类型、string、char类型等等)
数据绑定器
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService
类:在设置每一个值的时候,找它里面的所有converter,看哪个可以将这个数据类型(request带来参数的string)转换为指定的类型(比如JavaBean中int类型、string、char类型等等)
可以自定义 Converter:
处理表单,之前是name=pet.name/pet.age,现在是name=pet,默认的converter配置肯定识别不了,但如果就想用name=pet,如何将value拆分并封装为pet,那就要利用自定义converter
//1、WebMvcConfigurer定制化SpringMVC的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
// 将表单传的name=cat,value="猫,3“
//pet类里面的有name和age。这里source="猫,3"
//我们想自定义转换,将value中的猫给name,3给age
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;
}
});
}
};
}
目标方法执行完成,将将所有的数据都放在 ModelAndViewContainer。包含要去的页面地址View视图、Model数据。
处理派发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
@FunctionalInterfacepublic interface Converter<S, T>
8.数据响应及原理
响应处理包括数据响应和试图解析,视图解析另辟一段讲述,这里只讲述数据响应
1.响应JSON
springboot进行json
传输很简单:
- 导入web场景启动器
- 方法上使用
@ResponseBody
注解
其原理是因为web启动器里包含了json启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.4.11</version>
<scope>compile</scope>
</dependency>
而json
启动器里使用的是jackson
依赖jar包
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.4</version>
<scope>compile</scope>
</dependency>
2.数据响应原理(了解)
返回值处理器
从返回值处理器中选出能处理该类型的返回值处理器
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
- 返回值处理器判断是否支持这种类型返回值
supportsReturnType
- 返回值处理器调用
handleReturnValue
进行处理
SpringMVC支持哪些返回值
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult / ListenableFuture / CompletionStage
WebAsyncTask
被@ModelAttribute注解且为java类型的
被@ResponseBody注解的
开发中常用的@ResponseBody
是由RequestResponseBodyMethodProcessor返回值处理器处理返回值
RequestResponseBodyMethodProcessor类中使用消息转换器MessageConverters进行写出操作
RequestResponseBodyMethodProcessor类
@Override
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
返回值处理器里面有消息转换器,消息转换器中有内容协商,下面会介绍
消息转换器的作用: 每个消息转换器都通过canRead
(服务器读入)/canWrite
(服务器写出)看是否支持将此类型的对象转为MediaType类型的数据,如果可以就执行Read
/Write
方法
例如:Person(自定义的java类)转为JSON
下图是源码中通过每个消息转换器都支持些什么类型,然后看mediaType是否包含在里面,如果在则返回true表示能处理
有以下消息转换器
与上图对应能处理的目标类型
0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式xml处理的。
总的来说,RequestResponseBodyMethodProcessor返回值处理器处理返回值过程:(以java类型转为json为例)
- 利用内容协商得知浏览器希望接受什么类型数据(浏览器默认会以请求头的方式告诉服务器能接受什么样的内容类型)
- SpringMVC会挨个遍历所有容器底层的消息转换器 ,看哪个消息转换器能支持转为浏览器所希望的类型数据
- 最终找到
MappingJackson2HttpMessageConverter
消息转换器可以将java类型的数据转为json
MappingJackson2HttpMessageConverter
消息转换器的底层本质就是利用jackson
的objectMapper
方法将java类型对象转为json
3.内容协商原理(了解)
在看哪个消息转换器能处理的过程中,有个关键的一步就是内容协商
根据请求头Accept需要什么类型数据,服务器通过内容协商原理找到能转为该类型的消息转换器进行类型转换
之前消息转换器有java类型到json
的转换,我们现在导入jackson
处理xml
的包,实现java类型到xml
的转换(导入了jackson处理xml的包,java类型到xml转换的消息转换器就会自动进来)
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
下面源码解释了为什么导入了依赖就能自动添加MappingJackson2XmlHttpMessageConverter
java类型到xml的消息转换器
WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
内容协商过程:
- 判断当前响应头中是否已经有确定的媒体类型。MediaType
- 获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】
○contentNegotiationManager
内容协商管理器,默认使用基于请求头的策略
○ HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
- 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
- 找到支持操作Person的converter,把converter支持的媒体类型统计出来。
- 客户端需要【application/xml】。服务端能力【10种、json、xml】
- 进行内容协商的最佳匹配媒体类型
- 用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化
自定义希望服务器返回指定类型的数据
由于底层内容协商管理器默认使用基于请求头的策略,而浏览器每次发起请求Accept都是一样的,所以返回的数据不能自定义(Accept里有个返回类型权重,服务器根据权重最佳匹配一个消息转换器进行类型转换,不能由自己随心控制)。所以,这就需要开启浏览器参数方式内容协商功能
在配置文件中开启基于请求参数的指定类型数据返回
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
这样底层就会多了一个基于请求参数的的内容协商策略
format = xxx:想要返回xxx类型的数据,在发起请求时指定即可
发请求(想要服务器返回json类型的数据):http://localhost:8080/test/person?format=json
发请求(想要服务器返回xml类型的数据):http://localhost:8080/test/person?format=xml
自定义MessageConverter以及自定义内容协商管理器
在自己的配置类中重写extendMessageConverters
方法
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters{
......
}
@Override
public void contentNegotiationManager() {
......
}
}
}
9.视图解析原理(了解)
视图解析:sprongboot处理完请求,想要跳转到指定页面的过程
ViewNameMethodReturnValueHandler返回值处理器:负责处理返回值指定的视图
视图解析器获得xxxView
视图对象 ,然后视图对象调用render()
方法进行页面渲染
视图解析器原理:
- 字符串类型返回值如果以
forward:
开始,则ViewNameMethodReturnValueHandler
会new InternalResourceView(forwardUrl)
。InternalResourceView
的render
方法调用request.getRequestDispatcher(path).forward(request, response);
(即请求转发) - 字符串类型返回值如果以
redirect:
开始: 则ViewNameMethodReturnValueHandler
会new RedirectView()
。RedirectView
的render
方法调用response.sendRedirect(encodedURL)
(即重定向) - 字符串类型返回值是普通字符串:则
ViewNameMethodReturnValueHandler
会new ThymeleafView()
。ThymeleafView找到指定页面渲染,但还是请求转发
render(mv, request, response)方法:根据controller方法的String返回值得到 View 对象(View 定义了页面的渲染逻辑)
- 所有的视图解析器尝试是否能根据当前返回值得到View对象
- 得到了 redirect:/main.html --> Thymeleaf new RedirectView()
- ContentNegotiationViewResolver里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
- view.render(mv.getModelInternal(), request, response);视图对象调用自定义的render进行页面渲染工作
目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面(包括数据和视图),任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)