本文为B站尚硅谷 SpringBoot2 教学视频的笔记总结,连接:点我
1. Spring与SpringBoot
1.1. Spring能做什么
1.1.1. Spring生态
https://spring.io/projects/spring-boot
spring功能覆盖了:
web开发
数据访问
安全控制
分布式
消息服务
移动开发
批处理
…
1.1.2. Spring5重大升级
响应式编程
内部源码设计
使用了 Java8 的一些新特性,如:接口默认实现。重新设计源码架构。
1.2. 为什么用SpringBoot
spring能够快速创建出生产级别的Spring应用。
-
优点
-
创建独立Spring应用。
-
内嵌web服务器。
-
自动starter依赖,简化构建配置。
-
自动配置Spring以及第三方功能。
-
提供生产级别的监控、健康检查及外部化配置。
-
无代码生成、无需编写XML。
SpringBoot是整合Spring技术栈的一站式框架。
SpringBoot是简化Spring技术栈的快速开发脚手架。
-
-
SpringBoot缺点
-
人称版本帝,迭代快,需要时刻关注变化。
-
封装太深,内部原理复杂,不容易精通。
-
1.3. 时代背景
1.3.1. 微服务
微服务是一种架构风格。
一个应用可以拆分为一组小型服务。
每个服务运行在自己的进程内,可独立部署和升级。
服务之间使用轻量级HTTP交互。
服务围绕业务功能拆分。
可以利用全自动部署机制独立部署。
去中心化,服务自治。服务可以使用不同的语言、不同的存储技术。
1.3.2. 分布式
分布式的困难:
远程调用
服务发现
负载均衡
服务容错
配置管理
服务监控
链路追踪
日志管理
任务调度
…
分布式的解决
SpringBoot + SpringCloud
1.3.3. 云原生
原生应用如何上云。
上云的困难
服务自愈
弹性伸缩
服务隔离
自动化部署
灰度发布
流量治理
…
上云的解决
2. SpringBoot2入门
2.1. 系统要求
Java 8 & 兼容java14
Maven 3.3+
IntelliJ IDEA 2019.1.2
2.1.1. Maven配置文件
在本地maven的 settings.xml 文件中对应位置添加以下内容:
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
<profiles>
<profile>
<id>jdk-1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
</profile>
</profiles>
2.2. HelloWorld项目
需求:浏览发送 /hello 请求,响应 “Hello,Spring Boot 2”
2.2.1. 创建maven工程
2.2.2. 引入依赖
在创建的项目的 pom 文件正确位置添加以下内容;
<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>
2.2.3. 创建主程序
以下是主程序代码:
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
2.2.4. 编写业务
创建 controller 类,接受 /hello 请求并处理
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle01() {
return "Hello, Spring Boot 2!";
}
}
2.2.5. 运行&测试
运行MainApplication
类
浏览器输入http://localhost:8888/hello,将会输出Hello, Spring Boot 2!。
2.2.6. 配置参数
在 maven 工程的 resources 文件夹中创建application.properties文件,然后添加以下代码
# 3. 设置端口号
server.port=8888
3.1. 打包部署
在pom.xml添加以下代码插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
注意,对于 sprintBoot 官网提供的很多依赖和插件,都不需要手动指定版本号,在 springBoot 中都已经指定好了。
在IDEA的Maven插件上点击运行 clean
、package
,把helloworld工程项目的打包成jar包,
打包好的jar包被生成在helloworld工程项目的target文件夹内。
用cmd运行java -jar boot-01-helloworld-1.0-SNAPSHOT.jar
,既可以运行helloworld工程项目。
如果需要部署到服务器,只需要将jar包上传到服务器,然后直接在服务器执行即可。
4. 自动配置
4.1. SpringBoot特点
4.1.1. 依赖管理
springBoot 项目通过父项目做依赖管理。
<!--依赖管理,自己的项目只需要引入该父项目,即可引入了很多 sprintBoot 项目的其他依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<!--在上面项目的父项目如下,它几乎声明了所有开发中常用的依赖的版本号,提供了自动版本仲裁机制-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
开发导入starter场景启动器,可实现以下功能:
-
可以通过引入的依赖看到很多
spring-boot-starter-*
:其中*
表示某种场景。 -
只要引入starter,这个场景的所有常规需要的依赖都会自动引入
-
*-spring-boot-starter
:以这种方式命名的,是第三方为我们提供的简化开发的场景启动器。 -
所有场景启动器最底层的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.3.4.RELEASE</version> <scope>compile</scope> </dependency>
无需关注版本号,自动版本仲裁:
-
引入依赖默认都可以不写版本号。
-
引入非版本仲裁的依赖,要写版本号。
可以修改默认版本号:
-
查看spring-boot-dependencies里面规定当前依赖的版本用的 key。
-
在当前项目里面重写配置,如下面的代码:
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
4.1.2. 自动配置
-
自动配好Tomcat。
-
引入Tomcat依赖。
-
配置Tomcat。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <version>2.3.4.RELEASE</version> </dependency>
-
-
自动配好SpringMVC
-
引入SpringMVC全套组件
-
自动配好SpringMVC常用组件(功能)
-
-
自动配好Web常见功能,如:字符编码问题
- SpringBoot帮我们配置好了所有web开发的常见场景
-
默认的包结构
-
主程序所在包及其所有子包里面的组件都会被默认扫描
-
无需进行包扫描配置
-
如果想要改变扫描路径,可使用以下方式
-
@SpringBootApplication(scanBasePackages=“com.atguigu”)
-
@ComponentScan 指定扫描路径
@SpringBootApplication
等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(“com.atguigu”)
-
-
-
各种配置拥有默认值
-
默认配置最终都是映射到某个类上,如:MultipartProperties
-
配置文件的值最终会绑定到对应的类上,这些类会在容器中创建对象
-
-
按需加载所有自动配置项
-
自己开发的项目中有非常多的starter
-
引入了哪个场景,这个场景的自动配置才会开启
-
SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面
-
4.2. 容器功能
4.2.1. 组件添加
4.2.1.1. @Component、@Controller、@Service、@Repository
它们是Spring的基本标签,在Spring Boot中并未改变它们原来的功能。
4.2.1.2. @Configuration
配置类,将该类中所有方法返回的对象注册到容器中。
/**
* 1、配置类里面使用 @Bean 标注在方法上,表示将方法的返回值对象注册到容器中,默认单实例
* 2、配置类本身也是组件,也会被注册到容器中。
* 3、proxyBeanMethods:代理bean的方式
* Full(proxyBeanMethods = true):保证每个标注有 @Bean 的方法返回的组件都为单实例,多次调用返回的都是同一个对象
* Lite(proxyBeanMethods = false):每个标注有 @Bean 的方法,每次被调用,返回的组件都是新创建的
* 组件依赖必须使用Full模式,默认,也就是该属性值为 true。
*/
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类
public class MyConfig {
/**
* Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册到容器中的单实例对象
*/
@Bean //给容器中添加组件,以方法名作为组件的id,返回值类型就是组件的类型,返回的值,就是组件在容器中的实例
public User user01(){
User zhangsan = new User("zhangsan", 18);
//user组件依赖了Pet组件,如果有组件依赖,则必须使用 full 模式,也就是设置 proxyBeanMethods 属性为 true。
zhangsan.setPet(tomcatPet());
return zhangsan;
}
@Bean("tom")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
public class MainApplication {
public static void main(String[] args) {
//1、返回IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
//3、从容器中获取组件
Pet tom01 = run.getBean("tom", Pet.class);
Pet tom02 = run.getBean("tom", Pet.class);
System.out.println("组件:"+(tom01 == tom02));
//4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892
MyConfig bean = run.getBean(MyConfig.class);
System.out.println(bean);
//如果@Configuration(proxyBeanMethods = true),代理对象调用方法,SpringBoot总会检查容器中是否有这个组件。
//保持组件单实例,如果设置 proxyBeanMethods 为 true ,则下面两次获取的都是同一个对象
User user = bean.user01();
User user1 = bean.user01();
System.out.println(user == user1);
User user01 = run.getBean("user01", User.class);
Pet tom = run.getBean("tom", Pet.class);
System.out.println("用户的宠物:"+(user01.getPet() == tom));
}
}
最佳实战
-
配置类组件之间无依赖关系用Lite模式,可以加速容器启动过程,减少判断
-
配置类组件之间有依赖关系,方法会被调用,以得到之前单实例组件,用Full模式
4.2.1.3. @Import
配合 @Configuration
注解使用
@Import({User.class, DBHelper.class})
:给容器中自动创建出这两个类的组件,默认组件的名字就是全类名。
4.2.1.4. @Conditional
条件装配:满足Conditional指定的条件,则进行组件注入
@ConditionalOnBean(name = "tom") //容器中有名称为 tom 的组件时,才会给容器中注册 MyConfig 类的对象。
@ConditionalOnMissingBean(name = "tom") //容器中没有名称为 tom 的组件时,才会给容器中注册 MyConfig 类的对象。
public class MyConfig {}
4.2.2. 原生配置文件引入
4.2.2.1. @ImportResource
导入原生配置文件
@ImportResource("classpath:beans.xml")
:导入Spring的配置文件,如果旧项目中使用
spring 的 xml 文件来管理组件,在 sprintBoot 中,可以通过该注解将之前的配置文件导入进来,无需将 xml 文件功能使用 java 代码重新实现一遍。
4.2.3. 配置绑定
使用Java读取properties文件中的内容,并且把它封装到JavaBean中,以供随时使用
传统方法:
public class GetProperties {
public static void main(String[] args) throws FileNotFoundException, IOException {
Properties pps = new Properties();
pps.load(new FileInputStream("a.properties"));
Enumeration enum1 = pps.propertyNames();//得到配置文件的名字
while (enum1.hasMoreElements()) {
String strKey = (String) enum1.nextElement();
String strValue = pps.getProperty(strKey);
System.out.println(strKey + "=" + strValue);
//封装到JavaBean。
}
}
}
4.2.3.1. @ConfigurationProperties + @Component
假设有配置文件application.properties,并有以下内容:
mycar.brand=BYD
mycar.price=100000
只有包含在容器中的组件,才会拥有SpringBoot提供的强大功能,因此需要将配置文件绑定到某个组件,并且将组件注册到容器中,可使用如下方式进行组件注册,以及将组件和配置文件绑定:
/**
* ToString:生成 toString 方法
* Data:生成 getter、setter 方法
* Component:将该组件注册到容器中
* ConfigurationProperties:表示该组件和配置文件中的值进行绑定。
* prefix:表示要绑定的配置文件中内容的 key 的前缀。
*/
@ToString
@Data
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
private String brand;
private Integer price;
}
4.2.3.2. @EnableConfigurationProperties + @ConfigurationProperties
/**
* EnableConfigurationProperties:把这个Car这个组件自动注册到容器中,开启Car类和配置文件绑定功能
*/
@EnableConfigurationProperties(Car.class)
public class MyConfig {
...
}
/**
* ConfigurationProperties:表示该组件和配置文件中的值进行绑定。
*/
@ConfigurationProperties(prefix = "mycar")
public class Car {
...
}
推荐使用上面的方式进行组件和配置的绑定。
4.3. 自动配置
SpringBoot默认会在底层配好所有的组件,但是如果用户配置了自己的组件,则以用户的优先。
总结:
-
SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
-
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。(xxxxProperties里面读取,xxxProperties和配置文件进行了绑定)。
-
生效的配置类就会给容器中装配很多组件
-
只要容器中有这些组件,相当于这些功能就有了
-
定制化配置
-
用户直接自己
@Bean
替换底层的组件,@Bean
注解添加到方法上,表示方法返回的值可以被注册到组件中。 -
用户根据这个组件获取的是配置文件的什么值,然后就可以去修改对应的值。
自动配置整体流程:xxxxxAutoConfiguration —> 组件 —>
xxxxProperties里面拿值 ----> application.properties
-
4.3.1. 最佳实践
-
引入场景依赖:官方文档
-
查看自动配置了哪些(选做)
-
自己分析,引入场景对应的自动配置一般都生效了
-
配置文件中
debug=true
开启自动配置报告。Negative
(不生效),Positive
(生效);
-
-
是否需要修改
-
参照文档修改配置项:官方文档
-
自己分析,xxxxProperties绑定了配置文件的哪些值。
-
-
自定义加入或者替换组件
- @Bean、@Component…
-
自定义器 XXXXXCustomizer;
4.4. 开发小技巧
4.4.1. Lombok
简化JavaBean开发
Lombok 使用注解方式代替构造器、getter/setter、toString()等冗余代码。
spring boot已经管理Lombok,只需在自己的项目中引入依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
然后给IDEA安装插件:IDEA中File->Settings->Plugins,搜索安装Lombok插件。
@NoArgsConstructor
//@AllArgsConstructor
@Data
@ToString
@EqualsAndHashCode
public class User {
private String name;
private Integer age;
private Pet pet;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
}
@Slf4j
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle01(@RequestParam("name") String name){
log.info("请求进来了....");
return "Hello, Spring Boot 2!"+"你好:"+name;
}
}
4.4.2. Spring Initializer(项目初始化向导)
Spring Initializer是创建Spring Boot工程向导。
在IDEA中,菜单栏New -> Project -> Spring Initializer。
-
在创建项目时,选择需要使用到的场景
-
项目创建好之后,观察 pom 文件,就会发现,已经引入了我们上一步勾选到的所有场景用到的依赖
-
自动创建好了标准的目录结构
-
自动编写好主类
5. 配置文件
5.1. 文件类型
5.1.1. properties
同以前的properties用法
5.1.2. yaml
5.1.2.1. 简介
YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。
非常适合用来做以数据为中心的配置文件。
5.1.2.2. 基本语法
-
key: value;kv之间有空格
-
大小写敏感
-
使用缩进表示层级关系
-
缩进不允许使用tab,只允许使用空格
-
缩进的空格数不重要,只要相同层级的元素左对齐即可
-
#
表示注释 -
字符串无需加引号,如果有一些特殊字符,则添加单引号
'
,单引号括起来的字符串中的所有字符,会保持原样不动。双引号""表示字符串内容会被转义,也就是说,如果字符串中出现了\n
,则该字符最后会被转义为换行符进行展现。
5.1.2.3. 数据类型
-
字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
-
对象:键值对的集合。map、hash、set、object
#行内写法: map1: { k1:v1,k2:v2,k3:v3 } #或 map2: k1: v1 k2: v2 k3: v3
-
数组:array、list、queue
#行内写法: k1: [ v1,v2,v3 ] #或者 k2: - v1 - v2 - v3
5.1.2.4. 示例
@ConfigurationProperties(prefix = "person")
@Component
@ToString
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
@ToString
@Data
public class Pet {
private String name;
private Double weight;
}
yaml文件表示以上对象
person:
# 单引号会将 \n 作为字符串输出 双引号会将 \n 作为换行输出
boss: true
birth: 2019/12/9
age: 18
# interests: [篮球,足球]
interests:
- 篮球
- 足球
animal: [ 阿猫,阿狗 ]
# score:
# english: 80
# math: 90
score: { english:80,math:90 }
salarys:
- 9999.98
- 9999.99
pet:
name: 阿狗
weight: 99.99
allPets:
sick:
- { name: 阿狗,weight: 99.99 }
- name: 阿猫
weight: 88.88
- name: 阿虫
weight: 77.77
health:
- { name: 阿花,weight: 199.99 }
- { name: 阿明,weight: 199.99 }
# 如果出现了大写字母,除了和 bean 类属性完全保持一致外,也可以使用 -小写字母 来表示大写字母
user-name: zhangsan
5.2. 配置提示
自定义的类和配置文件绑定一般没有提示,如果想要在配置文件中编写自定义类对应属性时出现提示,则可以在 pom 文件中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 下面插件作用是工程打包时,不将spring-boot-configuration-processor打进包内,让其只在编码的时候有用 -->
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
配置好之后,重新运行一下项目的启动类,之后在配置文件中编写配置类对应属性时,就会出现提示。
项目启动类只需要运行一次即可,无需一直运行。
6. Web开发
6.1. SpringMVC自动配置概览
-
大多场景我们都无需自定义配置
-
内容协商视图解析器和BeanName视图解析器
-
支持静态资源(包括webjars)
-
自动注册
Converter,GenericConverter,Formatter
-
支持
HttpMessageConverters
,对接收的 Http 请求内容进行各种自动转换 -
自动注册
MessageCodesResolver
(国际化用) -
静态 index.html 页支持,首页
-
支持自定义 Favicon
-
自动使用
ConfigurableWebBindingInitializer
,(DataBinder负责将请求数据绑定到JavaBean上) -
不用@EnableWebMvc注解,使用
@Configuration + WebMvcConfigurer
自定义规则 -
声明 WebMvcRegistrations 改变默认底层组件
-
使用
@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration
全面接管SpringMVC
6.2. 简单功能分析
6.2.1. 静态资源访问
6.2.1.1. 静态资源目录
只要静态资源放在类路径下,比如:/static
(或者是 /public
、 /resources
、/META-INF/resources
),就可以直接访问
访问方式:当前项目根路径 / + 静态资源名
原理:静态映射 /**
。
请求进来之后,先去找Controller看能不能处理。不能处理的所有请求将会交给静态资源处理器,如果静态资源也找不到,则响应404页面。
改变默认的静态资源路径:
spring:
mvc:
# 表示访问静态资源时,需要在最前面添加 res 进行访问,以和其他的访问区分开。注意,如果配置了该参数,则默认的首页访问将会失效
static-path-pattern: /res/**
resources:
# 表示访问的静态资源在 /haha 目录下,而不是默认的资源路径下。
static-locations: [ classpath:/haha/ ]
6.2.1.2. 静态资源访问前缀
默认无前缀
spring:
mvc:
static-path-pattern: /res/**
当前项目 + static-path-pattern + 静态资源名 = 在静态资源文件夹下找
6.2.1.3. webjar
可用jar方式添加css,js等资源文件:https://www.webjars.org/,项目会自动映射
/webjars/**
例如,添加jquery:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
访问地址为:http://localhost:8080/webjars/jquery/3.5.1/jquery.js,后面地址要按照依赖里面的包路径编写。
6.2.2. 欢迎页支持
静态资源路径下添加 index.html 文件。
-
可以配置静态资源路径。
-
但是不可以配置静态资源的访问前缀,否则会导致index.html不能被默认访问。
spring: # mvc: # static-path-pattern: /res/** 这个会导致welcome page功能失效 resources: static-locations: [ classpath:/haha/ ]
controller能自动处理 /index
请求。
6.2.3. 自定义Favicon
Favicon指网页标签上的小图标。
将favicon.ico 放在静态资源目录下即可。
spring:
# 7. mvc:
# 8. static-path-pattern: /res/** 这个会导致 Favicon 功能失效
8.1. 请求参数处理
8.1.1. 请求映射
8.1.1.1. rest使用与原理
-
@xxxMapping:
@GetMapping
、@PostMapping
、@PutMapping
、@DeleteMapping
-
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
-
以前:
/getUser
获取用户、/deleteUser
删除用户、/editUser
修改用户、/saveUser
保存用户、 -
现在:
/user
GET-获取用户、DELETE-删除用户、PUT-修改用户、POST-保存用户
-
-
核心Filter;HiddenHttpMethodFilter
-
用法:开启页面表单的Rest功能,页面 form的属性method=post,隐藏域
_method=put
、delete等(如果直接get或post,无需隐藏域) -
SpringBoot中手动开启 filter 支持
spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能
-
-
之后就可以在 controller 处理中使用了:
@RequestMapping("/bug.jpg") public String hello() { //request return "aaaa"; } // @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-张三"; } @DeleteMapping("/user") // @RequestMapping(value = "/user",method = RequestMethod.DELETE) public String deleteUser() { return "DELETE-张三"; }
-
扩展:将表单中的
_method
这个名称换成其他的-
在配置类中添加以下代码,将自定义的 HiddenHttpMethodFilter 注册到容器中
@Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m"); return methodFilter; }
-
Rest原理(表单提交要使用REST的时候)
-
表单提交会带上
_method=PUT
-
请求过来后被HiddenHttpMethodFilter拦截
-
请求是否正常,并且是POST
-
获取到
_method
的值。 -
兼容以下请求;
PUT
、DELETE
、PATCH
-
原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
-
过滤器链放行的时候使用wrapper,所以以后的方法调用getMethod时,调用的是 requesWrapper 实现的方法。
-
-
Rest使用客户端工具:如PostMan可直接发送put、delete等方式请求,无需
fliter。
8.1.2. 普通参数与基本注解
8.1.2.1. 注解
@PathVariable
:从 rest 风格的请求路径中获取对应变量的值
@RequestHeader
: 获取请求头
@RequestParam
: 获取请求参数(问号后的参数,url?a=1&b=2)
@CookieValue
: 获取Cookie值
@RequestAttribute
: 获取request域属性
@RequestBody
: 获取请求体[POST]
@MatrixVariable
: 矩阵变量
示例:
@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;
}
}
RequestAttribute
用例:
@Controller
public class RequestController {
@GetMapping("/goto")
public String goToPage(HttpServletRequest request) {
request.setAttribute("msg", "成功了...");
request.setAttribute("code", 200);
return "forward:/success"; // 转发到 /success请求
}
@GetMapping("/params")
public String testParam(Map<String, Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response) {
map.put("hello", "world666");
// 通过 module 对象添加的属性值,通过转发之后,最后也会放到 request 对象中,之后也可以获取到
model.addAttribute("world", "hello666");
request.setAttribute("message", "HelloWorld");
Cookie cookie = new Cookie("c1", "v1");
response.addCookie(cookie);
return "forward:/success";
}
@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg", required = false) String msg,
@RequestAttribute(value = "code", required = false) Integer code,
HttpServletRequest request) {
// 查看运行结果得知,通过 @RequestAttribute 注解获取请求对象属性值,和通过 request.getAttribute 方法获取的值,都是一样的。
Object msg1 = request.getAttribute("msg");
Map<String, Object> map = new HashMap<>();
Object hello = request.getAttribute("hello");
Object world = request.getAttribute("world");
Object message = request.getAttribute("message");
map.put("reqMethod_msg", msg1);
map.put("annotation_msg", msg);
map.put("hello", hello);
map.put("world", world);
map.put("message", message);
return map;
}
}
Map、Model(map、model里面的数据会被放在request的请求域
request.setAttribute)
@MatrixVariable与UrlPathHelper
语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
SpringBoot默认禁用矩阵变量的功能
手动开启:将UrlPathHelper
的removeSemicolonContent
设置为false,让其支持矩阵变量。
矩阵变量必须有url路径变量才能被解析
手动开启矩阵变量:
-
实现WebMvcConfigurer接口:
@Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除 : 后面的内容,矩阵变量的功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }
-
或者是在配置类中创建返回WebMvcConfigurerBean:
@Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除 ; 后面的内容,矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } } } }
@MatrixVariable使用案例
@RestController
public class ParameterTestController {
// /cars/sell;low=34;brand=byd,audi,yd
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path) {
Map<String, Object> map = new HashMap<>();
map.put("low", low);
map.put("brand", brand);
map.put("path", path);
return map;
}
// /boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
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;
}
}
8.2. 数据响应与内容协商
8.2.1. HTTPMessageConverter原理
返回值处理器ReturnValueHandler原理:
返回值处理器判断是否支持这种类型返回值
supportsReturnType
返回值处理器调用
handleReturnValue
进行处理
RequestResponseBodyMethodProcessor
可以处理返回值标了@ResponseBody
注解的。利用
MessageConverters
进行处理,将数据写为json
内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型):
服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
SpringMVC会挨个遍历所有容器底层的
HttpMessageConverter
,看谁能处理得到
MappingJackson2HttpMessageConverter
可以将对象写为json利用
MappingJackson2HttpMessageConverter
将对象转为json再写出去。
8.2.2. 内容协商
根据客户端接收能力不同,返回不同媒体类型的数据。
引入XML依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
Http协议中规定,Accept字段告诉服务器本客户端可以接收的数据类型:
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
发请求:http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
Http协议中规定,Accept字段告诉服务器本客户端可以接收的数据类型。
可用Postman软件分别测试返回json和xml:只需要改变请求头中Accept字段(application/json
、application/xml
)。
内容协商原理:
-
判断当前响应头中是否已经有确定的媒体类型MediaType。
-
获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段application/xml)
-
contentNegotiationManager 内容协商管理器,默认使用基于请求头的策略
-
HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
-
-
遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
-
找到支持操作Person的converter,把converter支持的媒体类型统计出来。
-
客户端需要application/xml,服务端有10种MediaType。
-
进行内容协商的最佳匹配媒体类型
-
查找到支持将对象转为最佳匹配媒体类型的converter,调用它进行转化。
导入了jackson处理xml的包,xml的converter就会自动进来
8.2.2.1. MessageConverter
实现多协议数据兼容:json、xml、x-guigu(这个是自创的)
-
@ResponseBody
响应数据出去,调用 RequestResponseBodyMethodProcessor 处理 -
Processor 处理方法返回值,通过 MessageConverter 处理
-
所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
-
内容协商找到最终的 messageConverter
SpringMVC 要实现什么功能,只需要在配置入口给容器中添加一个
WebMvcConfigurer 即可。
以下是代码示例:
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
/**
* 自定义Converter
*/
public class GuiguMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class);
}
/**
* 服务器要统计所有MessageConverter都能写出哪些内容类型
* application/x-guigu
*
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-guigu");
}
@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// 自定义协议数据的写出
String data = person.getUserName() + ";" + person.getAge() + ";" + person.getBirth();
// 写出去
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
};
}
}
@Controller
public class ResponseTestController {
/**
* 1、浏览器发请求直接返回 xml [application/xml] jacksonXmlConverter
* 2、如果是ajax请求 返回 json [application/json] jacksonJsonConverter
* 3、如果硅谷app发请求,返回自定义协议数据 [appliaction/x-guigu] xxxxConverter,返回值格式为:属性值1;属性值2;
* <p>
* 步骤:
* 1、添加自定义的MessageConverter进系统底层
* 2、系统底层就会统计出所有MessageConverter能操作哪些类型
* 3、客户端内容协商 [guigu--->guigu]
* <p>
*/
@ResponseBody // 利用返回值处理器里面的消息转换器进行处理
@GetMapping(value = "/test/person")
public Person getPerson() {
Person person = new Person();
person.setAge(28);
person.setBirth(new Date());
person.setUserName("zhangsan");
return person;
}
}
启动项目后,用Postman发送/test/person
(请求头Accept:application/x-guigu
),将返回自定义协议数据的内容。
日后开发要注意,有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
8.3. 视图解析
视图解析:SpringBoot默认不支持
JSP,需要引入第三方模板引擎技术实现页面渲染。
视图解析原理流程
-
目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
-
方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
-
任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。
-
processDispatchResult 处理派发结果(页面该如何响应)
-
render(mv, request, response); 进行页面渲染逻辑
-
根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
-
所有的视图解析器尝试是否能根据当前返回值得到View对象
-
得到了 redirect:/main.html --> Thymeleaf new RedirectView()
-
ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
-
view.render(mv.getModelInternal(), request, response);视图对象调用自定义的render进行页面渲染工作
RedirectView 如何渲染【重定向到一个页面】- 获取目标url地址
- response.sendRedirect(encodedURL);
-
-
-
视图解析:
-
返回值以
forward:
开始: new InternalResourceView(forwardUrl); --> 转发 request.getRequestDispatcher(path).forward(request, response); -
返回值以
redirect:
开始: new RedirectView() --》 render就是重定向 -
返回值是普通字符串: new ThymeleafView()—>
8.4. 拦截器
登录检查与静态资源放行
8.4.1. 自定义拦截器
-
编写一个拦截器实现
HandlerInterceptor
接口。 -
然后将拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors())。
-
指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截】。
编写一个实现HandlerInterceptor接口的拦截器:
/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这个拦截器放在容器中
*/
@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", "请先登录");
response.sendRedirect("/"); // 重定向
request.getRequestDispatcher("/").forward(request, response); // 转发
return false;
}
/**
* 目标方法执行完成以后
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}", modelAndView);
}
/**
* 页面渲染以后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}", ex);
}
}
将拦截器注册到容器中并指定拦截规则:
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】,所以需要单独指定需要放行的静态资源
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截的请求,所有请求都被拦截包括静态资源
.excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**"); // 放行的请求
}
}
8.4.2. 拦截器执行时机和原理
-
根据当前请求,找到HandlerExecutionChain(可以处理请求的handler以及handler的所有拦截器)
-
先来顺序执行所有拦截器的 preHandle() 方法。
-
如果当前拦截器preHandle()返回为true,则执行下一个拦截器的preHandle()
-
如果当前拦截器返回为false,直接倒序执行所有已经执行了的拦截器的 afterCompletion()。
-
-
如果任何一个拦截器返回false,直接跳出不执行目标方法。
-
所有拦截器都返回true,才执行目标方法。
-
倒序执行所有拦截器的postHandle()方法。
-
前面的步骤有任何异常都会直接倒序触发 afterCompletion()。
-
页面成功渲染完成以后,也会倒序触发 afterCompletion()。
8.5. 文件上传
8.5.1. 页面表单代码
页面代码/static/form/form_layouts.html
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="headerImg" id="exampleInputFile">
</div>
<div class="form-group">
<label for="exampleInputFile">生活照</label>
<input type="file" name="photos" multiple>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
第一个 div 是单文件,第二个 div 是多文件。
8.5.2. 控制层代码
/**
* MultipartFile 自动封装上传过来的文件
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
// 单文件,需要使用 MultipartFile 类接收参数
@RequestPart("headerImg") MultipartFile headerImg,
// 多文件,需要使用 MultipartFile[] 数组来接收参数
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email,username,headerImg.getSize(),photos.length);
if(!headerImg.isEmpty()){
//保存到文件服务器,OSS服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("H:\\cache\\"+originalFilename));
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("H:\\cache\\"+originalFilename));
}
}
}
}
注意:如果 controller 中的方法接收了文件参数,则不可再使用@RequestBody
注解来将所有请求参数封装到 pojo 对象中接收,而只能使用@RequestParam
注解来接收单个参数。
文件上传相关的配置类:
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.MultipartProperties
文件大小相关配置项:
# 单文件大小
spring.servlet.multipart.max-file-size=10MB
# request请求大小
spring.servlet.multipart.max-request-size=100MB
10.1. 自动配置原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties,自动配置好了
StandardServletMultipartResolver 【文件上传解析器】
原理步骤:
-
请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
-
参数解析器解析请求中的文件内容,并封装成MultipartFile
-
将request中文件信息封装为一个Map:
MultiValueMap<String,MultipartFile>
可以使用FileCopyUtils工具类实现文件流的拷贝。
10.2. 异常处理
10.2.1. 错误处理
10.2.1.1. 默认规则
默认情况下,Spring Boot 会使用 /error
请求处理所有异常。
对于机器客户端(如PostMan),它将生成JSON响应,其中包含错误信息,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个"
whitelabel"错误视图,以HTML格式呈现相同的数据。
{
"timestamp":"2020-11-22T05:53:28.416+00:00",
"status":404,
"error": "Not Found",
"message": "No message available",
"path": "/asadada"
}
想要对其进行自定义,可以添加View并解析为error
要完全替换默认行为,可以实现
ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制并替换其内容。
/error/
目录下的4xx,5xx页面会被自动解析
10.2.1.2. 自定义错误处理实现
有三种实现方式。
- @ControllerAdvice+@ExceptionHandler 处理全局异常
/**
* 处理整个web controller的异常
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* ExceptionHandler 注解中的参数为要处理的异常类型,将来所有的异常类型,符合要求的,都会由该方法处理
*/
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String handleArithException(Exception e) {
log.error("异常是:{}", e);
return "login"; // 视图地址
}
}
- @ResponseStatus
/**
* ResponseStatus注解,表示处理服务器内部异常,依据是请求的状态码,符合指定状态码编号的异常,将会由该类处理。
*/
@ResponseStatus(code = HttpStatus.FORBIDDEN, reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {
public UserTooManyException() {
}
public UserTooManyException(String message) {
super(message);
}
}
- 自定义实现 HandlerExceptionResolver 接口
@Order(value = Ordered.HIGHEST_PRECEDENCE) // 优先级,数字越小优先级越高
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
try {
response.sendError(511, "我喜欢的错误");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
默认情况下,springBoot 会按照上面的优先级依次查找可以处理错误的处理器。
10.2.1.3. 自定义错误处理逻辑
-
自定义错误页
error/404.html
、error/5xx.html
,然后 sprintBoot 会进行匹配,匹配到精确错误状态码页面就匹配精确,没有就找
4xx.html
;如果都没有就触发白页。 -
@ControllerAdvice+@ExceptionHandler 处理全局异常,底层是有
ExceptionHandlerExceptionResolver
支持的。 -
@ResponseStatus+自定义异常 ,底层是
ResponseStatusExceptionResolver
。 responseStatus注解底层调用
response.sendError(statusCode, resolvedReason)
,使用tomcat的
/error 页面。 -
Spring底层的异常,如参数类型转换异常,是由DefaultHandlerExceptionResolver处理的:
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
-
自定义实现 HandlerExceptionResolver 处理异常,可以作为默认的全局异常处理规则
-
ErrorViewResolver 实现自定义异常处理:
-
response.sendError ,error请求就会转给controller
-
如果你的异常没有任何人能处理,则 tomcat底层调用 response.sendError,将error请求转给controller
-
-
basicErrorController 要去的页面地址是 ErrorViewResolver ;
10.3. Web原生组件注入
官方文档 - Servlets, Filters, and listeners
10.3.1. 使用原生的注解
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
@Slf4j
@WebFilter(urlPatterns = {"/css/*", "/images/*"}) // my
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request, response);
}
@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁");
}
}
最后还要在主启动类添加注解@ServletComponentScan注解
@ServletComponentScan(basePackages = "com.atguigu.admin")
@SpringBootApplication
public class Boot05WebAdminApplication {
public static void main(String[] args) {
SpringApplication.run(Boot05WebAdminApplication.class, args);
}
}
10.3.2. Spring方式注入
ServletRegistrationBean, FilterRegistrationBean,
ServletListenerRegistrationBean
@Configuration
public class MyRegistrConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MyServletContextListener myServletContextListener = new MyServletContextListener();
return new ServletListenerRegistrationBean(myServletContextListener);
}
}
10.4. 嵌入式Servlet容器
Spring Boot默认使用Tomcat服务器,若需更改其他服务器,则修改工程pom.xml:
<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>
10.5. 定制化原理
10.5.1. 定制化的常见方式
-
修改配置文件
-
xxxxxCustomizer
-
编写自定义的配置类 xxxConfiguration + @Bean 替换、增加容器中默认组件,视图解析器
-
Web应用:编写一个配置类实现 WebMvcConfigurer 即可定制化 web功能 + @Bean 给容器中再扩展一些组件
@Configuration public class AdminWebConfig implements WebMvcConfigurer { }
-
@EnableWebMvc + WebMvcConfigurer — @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置;实现定制和扩展功能(高级功能)。
原理:
-
WebMvcAutoConfiguration:默认的SpringMVC自动配置功能类,如静态资源、欢迎页等。
-
一旦使用
@EnableWebMvc
,就会@Import(DelegatingWebMvcConfiguration.class)
。 -
DelegatingWebMvcConfiguration
的作用,只保证SpringMVC最基本的使用-
把所有系统中的WebMvcConfigurer拿过来,所有功能的定制都是这些WebMvcConfigurer合起来一起生效。
-
自动配置了一些非常底层的组件,如RequestMappingHandlerMapping,这些组件依赖的组件都是从容器中获取,比如:
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
-
-
WebMvcAutoConfiguration里面的配置要能生效必须
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
。 -
@EnableWebMvc 导致WebMvcAutoConfiguration 失效。
10.5.2. 原理分析套路
场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties -
绑定配置文件项。
11. 数据访问
11.1. SQL
11.1.1. 数据源的自动配置-HikariDataSource
11.1.1.1. 导入JDBC场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
接着导入数据库驱动包(MySQL为例)。
<!--默认版本:-->
<mysql.version>8.0.22</mysql.version>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--<version>5.1.49</version>-->
</dependency>
<!--
想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、重新声明版本(maven的属性的就近优先原则)
-->
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.49</mysql.version>
</properties>
11.1.1.2. 分析自动配置
-
DataSourceAutoConfiguration
: 数据源的自动配置。 -
修改数据源相关的配置:
spring.datasource
。 -
数据库连接池的配置:自己容器中没有DataSource才自动配置的。
-
底层配置好的连接池是:HikariDataSource。
@Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration
-
DataSourceTransactionManagerAutoConfiguration
:事务管理器的自动配置。 -
JdbcTemplateAutoConfiguration:JdbcTemplate
的自动配置,可以来对数据库进行CRUD。-
可以修改前缀为
spring.jdbc
的配置项来修改JdbcTemplate
。 -
@Bean @Primary JdbcTemplate
:Spring容器中有这个JdbcTemplate组件,使用@Autowired
。
-
-
JndiDataSourceAutoConfiguration:JNDI的自动配置。
-
XADataSourceAutoConfiguration:分布式事务相关的。
11.1.1.3. 修改配置项
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
11.1.1.4. 单元测试数据源
@Slf4j
@SpringBootTest(classes = {JdbcTemplate.class})
class Boot05WebAdminApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
// 用@org.junit.Test会报空指针异常,可能跟JUnit新版本有关
void contextLoads() {
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("记录总数:{}", aLong);
}
}
11.1.2. 使用Druid数据源
11.1.2.1. druid官方github地址
Druid是数据库连接池,能够提供强大的监控和扩展功能。
Spring Boot整合第三方技术的两种方式:
-
自定义
-
找starter场景
11.1.2.2. 自定义方式
-
添加依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.17</version> </dependency>
-
配置Druid数据源
@Configuration public class MyDataSourceConfig { // 默认的自动配置是判断容器中没有才会配:@ConditionalOnMissingBean(DataSource.class),因此这儿手动注册了组件,自动配置就会失效 @ConfigurationProperties("spring.datasource") @Bean public DataSource dataSource() throws SQLException { DruidDataSource druidDataSource = new DruidDataSource(); // 加入监控功能 druidDataSource.setFilters("stat,wall"); druidDataSource.setMaxActive(10); return druidDataSource; } }
-
-
配置Druid的监控页功能:
-
Druid内置提供了一个StatViewServlet用于展示Druid的统计信息。官方文档 - 配置_StatViewServlet配置。这个StatViewServlet的用途包括:
-
提供监控信息展示的html页面
-
提供监控信息的JSON API
-
Druid内置提供一个StatFilter,用于统计监控信息。官方文档 - 配置_StatFilter
-
WebStatFilter用于采集web-jdbc关联监控的数据,如SQL监控、URI监控。官方文档 - 配置_配置WebStatFilter
-
Druid提供了WallFilter,它是基于SQL语义分析来实现防御SQL注入攻击的。官方文档 - 配置 wallfilter
-
-
11.1.2.3. starter整合方式
-
引入依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.17</version> </dependency>
-
配置示例:
spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver druid: aop-patterns: com.atguigu.admin.* #监控SpringBean filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: # 配置监控页功能 enabled: true login-username: admin login-password: admin resetEnable: false web-stat-filter: # 监控web enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: stat: # 对上面filters里面的stat的详细配置 slow-sql-millis: 1000 logSlowSql: true enabled: true wall: enabled: true config: drop-table-allow: false
分析自动配置
扩展配置项 spring.datasource.druid
DruidSpringAopConfiguration.class,
监控SpringBean,配置项:spring.datasource.druid.aop-patternsDruidStatViewServletConfiguration.class,
监控页的配置:spring.datasource.druid.stat-view-servlet,默认开启DruidWebStatFilterConfiguration.class,
web监控配置:spring.datasource.druid.web-stat-filter,默认开启DruidFilterConfiguration.class: 所有Druid自己filter的配置
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat"; private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config"; private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding"; private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j"; private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j"; private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2"; private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log"; private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
SpringBoot配置示例:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
配置项列表:https://github.com/alibaba/druid/wiki/DruidDataSource配置属性列表
11.1.3. 整合MyBatis操作
starter的命名方式:
-
SpringBoot官方的Starter:spring-boot-starter-*
-
第三方的: *-spring-boot-starter
引入依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
11.1.3.1. 配置模式
-
项目全局配置文件
-
SqlSessionFactory:已经自动配置好了
-
SqlSession:自动配置了SqlSessionTemplate 组合了SqlSession
-
@Import(AutoConfiguredMapperScannerRegistrar.class)
-
-
@Mapper:只要我们写的操作MyBatis的接口标注了@Mapper就会被自动扫描进来
// MyBatis配置项绑定类 @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }) @ConditionalOnSingleCandidate(DataSource.class) @EnableConfigurationProperties(MybatisProperties.class) @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) public class MybatisAutoConfiguration implements InitializingBean
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX) public class MybatisProperties
public static final String MYBATIS_PREFIX = "mybatis";
修改配置文件中所有的 mybatis 开头的:
spring:
datasource:
username: root
password: 1234
url: jdbc:mysql://localhost:3306/my
driver-class-name: com.mysql.jdbc.Driver
# 12. 配置mybatis规则
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置
mapper-locations: classpath:mybatis/*.xml #sql映射文件位置
Mapper接口对应配置文件:
<?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.lun.boot.mapper.UserMapper">
<select id="getUser" resultType="com.lun.boot.bean.User">
select * from user where id=#{id}
</select>
</mapper>
Mapper 接口开发:
@Mapper
public interface UserMapper {
public User getUser(Integer id);
}
POJO:
public class User {
private Integer id;
private String name;
// getters and setters...
}
DB:
CREATE TABLE `user`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 3
DEFAULT CHARSET = utf8mb4;
Controller and Service:
@Controller
public class UserController {
@Autowired
private UserService userService;
@ResponseBody
@GetMapping("/user/{id}")
public User getUser(@PathVariable("id") Integer id) {
return userService.getUser(id);
}
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUser(Integer id) {
return userMapper.getUser(id);
}
}
配置private Configuration configuration;
也就是配置mybatis.configuration
相关内容,就是相当于修改mybatis全局配置文件中的值。(也就是说配置了mybatis.configuration,就不需配置mybatis全局配置文件了)
# 13. 配置mybatis规则
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
# 14. 可以不写全局配置文件,所有全局配置文件的配置都放在configuration配置项中即可
小结
-
导入MyBatis官方Starter。
-
编写Mapper接口,需标注@
Mapper
注解。 -
编写SQL映射文件并绑定Mapper接口。
-
在
application.yaml
中指定Mapper配置文件的所处位置,以及指定全局配置文件的信息(建议:配置在mybatis.configuration
下)。
14.1. 注解模式
Mapper接口:
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getById(Long id);
}
14.1.1. 混合模式
Mapper接口:
/**
* 方法上有注解的,使用的是注解模式,方法上没有注解的,使用的是配置文件模式,需要在对应的配置文件中有对应的sql。
*/
@Mapper
public interface UserMapper {
public User getUser(Integer id);
@Select("select * from user where id=#{id}")
public User getUser2(Integer id);
public void saveUser(User user);
@Insert("insert into user(`name`) values(#{name})")
@Options(useGeneratedKeys = true, keyProperty = "id")
public void saveUser2(User user);
}
Mapper文件:
<?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.lun.boot.mapper.UserMapper">
<select id="getUser" resultType="com.lun.boot.bean.User">
select *
from user
where id = #{id}
</select>
<insert id="saveUser" useGeneratedKeys="true" keyProperty="id">
insert into user(`name`)
values (#{name})
</insert>
</mapper>
最佳实践:
-
引入mybatis-starter
-
配置
application.yaml
,指定mapper-location
位置 -
编写Mapper接口并标注
@Mapper
注解-
简单方法直接注解方式
-
复杂方法编写mapper.xml进行绑定映射
-
-
将
@MapperScan(\"com.atguigu.admin.mapper\")
注解写到主程序类上后,其他的接口就可以不用标注@Mapper注解了,不过不推荐这么做,可能会引起阅读困难。@MapperScan("com.lun.boot.mapper") @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } }
14.1.2. 整合MyBatisPlus
14.1.2.1. 什么是MyBatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis
的基础上只做增强不做改变,为简化开发、提高效率而生。
建议安装 MybatisX 插件
14.1.2.2. 整合步骤
添加依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
自定配置原理:
-
MybatisPlusAutoConfiguration
为配置类,MybatisPlusProperties
为配置项绑定。 -
SqlSessionFactory会自动配置好,底层是容器中默认的数据源。
-
mapperLocations会自动配置好,有默认值
classpath:/mapper/**/*.xml
,这表示任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。建议将sql映射文件放在mapper目录下。 -
容器中也自动配置好了SqlSessionTemplate。
-
@Mapper
标注的接口也会被自动扫描,建议直接使用@MapperScan(\"com.lun.boot.mapper\")
批量扫描。
MyBatisPlus优点之一:只需要我们的Mapper继承MyBatisPlus的BaseMapper
接口就可以拥有CRUD能力,减轻开发工作。
/**
* 泛型为要操作的表对应的 pojo 类
*/
public interface UserMapper extends BaseMapper<User> {
}
14.1.2.3. CRUD功能
使用MyBatis Plus提供的IService
,ServiceImpl
,可以减轻Service层开发工作。
/**
* Service 的CRUD不用实现
*/
public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
}
添加分页插件:
@Configuration
public class MyBatisConfig {
/**
* MybatisPlusInterceptor,添加分页插件
*/
@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;
}
}
@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn",defaultValue = "1")Integer pn,
RedirectAttributes ra){
// removeById就是 MyBatis 提供的方法
userService.removeById(id);
ra.addAttribute("pn",pn);
return "redirect:/dynamic_table";
}
@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn", defaultValue = "1") Integer pn,
RedirectAttributes ra) {
userService.removeById(id);
ra.addAttribute("pn", pn);
return "redirect:/dynamic_table";
}
@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);
// userPage.getRecords()
// userPage.getCurrent()
// userPage.getPages()
model.addAttribute("users", userPage);
return "table/dynamic_table";
}
14.2. NoSQL
14.2.1. redis
Redis
是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。
Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence),并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
14.2.1.1. 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
自动配置:
-
RedisAutoConfiguration自动配置类 --> RedisProperties 属性类 -->
spring.redis.xxx
是对redis的配置。 -
连接工厂LettuceConnectionConfiguration、JedisConnectionConfiguration是准备好的,默认使用lettuce 客户端。
-
自动注入了
RedisTemplate<Object, Object>
,xxxTemplate。 -
自动注入了
StringRedisTemplate
,key,value都是String 类型。 -
底层只要我们使用StringRedisTemplate、RedisTemplate就可以操作Redis。
redis相关配置:
redis:
host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com
port: 6379
password: lfy:Lfy123456
client-type: lettuce
lettuce:
pool:
max-active: 10
min-idle: 5
尽量使用 host+port
的形式,而不是直接写 url ,防止 url 中的一些特殊字符造成无法解析
14.2.1.2. RedisTemplate与Lettuce
@Autowired
RedisTemplate redisTemplate;
@Test
void testRedis() {
ValueOperations<String, String> operations = redisTemplate.opsForValue();
operations.set("hello", "world");
String hello = operations.get("hello");
System.out.println(hello);
}
14.2.1.3. 切换至jedis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 如果想使用 jedis 客户端,则需要单独导入jedis 依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
相关配置:
redis:
host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com
port: 6379
password: lfy:Lfy123456
client-type: jedis
jedis:
pool:
max-active: 10
15. 单元测试
15.1. JUnit5的一些变化
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库。
JUnit 5官方文档
作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform
+ JUnit Jupiter
+ JUnit Vintage
JUnit Platform: Junit
Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。JUnit Jupiter: JUnit
Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit
Platform上运行。JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit
Vintage提供了兼容JUnit4.x,JUnit3.x的测试引擎。
注意:
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容JUnit4需要自行引入(不能使用JUnit4的功能 @Test)
JUnit 5’s Vintage已经从spring-boot-starter-test从移除。如果需要继续兼容Junit4需要自行引入Vintage依赖:
<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>
要使用添加JUnit 5,需添加对应的starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Spring的JUnit5的基本单元测试模板(Spring的JUnit4的是@SpringBootTest+@RunWith(SpringRunner.class)):
@SpringBootTest
class SpringBootApplicationTests {
@Autowired
private Component component;
@Test
//@Transactional 标注后连接数据库,对其操作有回滚功能
public void contextLoads() {
Assertions.assertEquals(5, component.getFive());
}
}
15.2. 常用注解
@Test:表示该方法是测试方法,但是与JUnit4的@Test不同,他的职责非常单一,不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
@ParameterizedTest:表示方法是参数化测试。
@RepeatedTest:表示方法可重复执行。
@DisplayName:为测试类或者测试方法设置展示名称。
@BeforeEach:表示在每个单元测试之前执行,执行多次。
@AfterEach:表示在每个单元测试之后执行,执行多次。
@BeforeAll:表示在所有单元测试之前执行,执行一次。
@AfterAll:表示在所有单元测试之后执行,执行一次。
@Tag:表示单元测试类别,类似于JUnit4中的@Categories。
@Disabled:表示测试类或测试方法不执行,类似于JUnit4中的@Ignore。
@Timeout:表示测试方法运行如果超过了指定时间将会返回错误。
@ExtendWith:为测试类或测试方法提供扩展类引用。
@DisplayName("junit5功能测试类")
public class Junit5Test {
@DisplayName("测试displayname注解")
@Test
void testDisplayName() {
System.out.println(1);
}
@ParameterizedTest
@ValueSource(strings = {"racecar", "radar", "able was I ere I saw elba"})
void palindromes(String candidate) {
// 参数有多少个,就执行多少次
assertTrue(StringUtils.hasLength(candidate));
System.out.println("断言成功了");
}
@Disabled
@DisplayName("测试方法2")
@Test
void test2() {
System.out.println(2);
}
@RepeatedTest(5)
void test3() {
System.out.println("重复执行的测试方法");
}
/**
* 规定方法超时时间。超出时间测试出异常
*
* @throws InterruptedException
*/
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(100);
}
@BeforeEach
void testBeforeEach() {
// 每个测试方法执行之前执行
System.out.println("单个测试开始了...");
}
@AfterEach
void testAfterEach() {
// 每个测试方法执行之后执行
System.out.println("单个测试结束了...\n");
}
@BeforeAll
static void testBeforeAll() {
// 只会在最开始执行一次
System.out.println("所有测试就要开始了...");
}
@AfterAll
static void testAfterAll() {
// 只会在结束之后 执行一次
System.out.println("所有测试结束了...");
}
}
15.3. 断言机制(assertions)
断言Assertion是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告。
15.3.1. 简单断言
用来对单个值进行简单的验证。如:
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);
assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);
assertFalse(1 > 2);
assertTrue(1 < 2);
assertNull(null);
assertNotNull(new Object());
}
注意,使用断言时,如果断言成功了,则没有任何输出,如果断言失败了,则会返回具体的错误信息,并且终止测试的执行。
15.3.2. 数组断言
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等,判断时,是根据数组的内容,一个一个进行比较的,所以如果两个数组中的内容一样,但是顺序不一样,也会判断为失败。
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[]{1, 2});
}
15.3.3. 组合断言
assertAll()方法接收多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。
@Test
@DisplayName("assert all")
public void all() {
assertAll(
"Math", // 错误提示信息
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 1)
);
}
15.3.4. 异常断言
在JUnit4时期,想要测试方法的异常情况时,需要用@Rule
注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式**Assertions.assertThrows()
**,配合函数式编程就可以使用。
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
// 扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));
}
该断言想要测试的是,我认为该方法应该抛出一个特定的异常,然后我来判断一下是不是,如果没有抛出该异常,我就认为断言失败了。上面这个例子,断言是成功的。当然了,如果是使用更大的异常类(父类)去判断,也是可以的。
15.3.5. 超时断言
JUnit5还提供了 Assertions.assertTimeout() 为测试方法设置超时时间。
@Test
@DisplayName("超时测试")
public void timeoutTest() {
// 如果测试方法运行时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
15.3.6. 快速失败
通过 fail 断言直接使测试失败。
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}
15.4. 前置条件(assumptions)
Unit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言assertions会使测试方法失败,而不满足的前置条件会使测试方法执行终止。
前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";
@Test
@DisplayName("simple")
public void simpleAssume() {
// 简单断言
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}
@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"), // 前置条件,如果不满足该条件,则下面的测试方法不会执行
() -> System.out.println("In DEV")
);
}
}
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。
assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,后面的测试方法并不会执行。
15.5. 嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach
和@AfterEach
注解,而且嵌套的层次没有限制。
@DisplayName("嵌套测试")
public class TestingAStackDemo {
Stack<Object> stack;
@BeforeEach
void testOuter(){
System.out.println("外层,每个方法执行前执行");
}
@ParameterizedTest
@DisplayName("参数化测试")
@ValueSource(ints = {1, 2, 3, 4, 5})
void testParameterized(int i) {
System.out.println(i);
}
@ParameterizedTest
@DisplayName("参数化测试")
@MethodSource("stringProvider")
void testParameterized2(String i) {
System.out.println(i);
}
static Stream<String> stringProvider() {
System.out.println("开始提供方法参数");
return Stream.of("apple", "banana", "atguigu");
}
@Test
@DisplayName("new Stack()")
void isInstantiatedWithNew() {
System.out.println("外层,断言 stack 对象是否为 null");
new Stack<>();
// 嵌套测试情况下,外层的Test不能驱动内层的Before(After)Each/All之类的方法运行
assertNull(stack);
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
System.out.println("内层第一层,给 stack 引用创建空对象");
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
System.out.println("内层第一层,断言 stack 是否为空");
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
System.out.println("内层第一层,断言移除顶层元素是否会报异常");
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
System.out.println("内层第一层,断言查看顶层元素是否会报异常");
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
System.out.println("内层第二层,给 stack 对象放入元素");
stack.push(anElement);
}
/**
* 内层的Test可以驱动外层的Before(After)Each/All之类的方法运行
*/
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
System.out.println("内层第二层,断言 stack 对象是否为空");
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
System.out.println("内层第二层,断言 stack 顶层对象是否为内容设置的对象,在移除顶层对象之后,stack 对象是否为空");
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
System.out.println("内层第二层,断言 stack 顶层对象是否为内容设置的对象,不移除顶层对象,stack 对象是否为空");
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
从上面的运行结果可以看出,测试都是先开始执行外部类的测试方法,然后再执行内部类的测试方法,并且外部类的 @BeforeEach 方法会在内部类的每个测试方法执行前都执行一次,不管是第几层内部类。
15.6. 参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试方法成为了可能,也为我们的单元测试带来许多便利。
利用@ValueSource
等注解指定入参,我们就可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
-
@ValueSource:为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
-
@NullSource: 表示为参数化测试提供一个null的入参
-
@EnumSource: 表示为参数化测试提供一个枚举入参
-
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
-
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
参数化测试支持外部的各类入参,如:CSV,YML,JSON 文件,甚至方法的返回值也可以作为入参。只需要实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") // 指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
// 注意需要返回一个 stream 流,并且是静态方法
return Stream.of("apple", "banana");
}
15.7. 迁移指南
在进行迁移的时候需要注意如下的变化:
-
注解在 org.junit.jupiter.api 包中,断言在org.junit.jupiter.api.Assertions 类中,前置条件在
org.junit.jupiter.api.Assumptions 类中。 -
把
@Before
和@After
替换成**@BeforeEach** 和**@AfterEach**。 -
把
@BeforeClass
和@AfterClass
替换成**@BeforeAll** 和**@AfterAll**。 -
把
@Ignore
替换成**@Disabled**。 -
把
@Category
替换成**@Tag**。 -
把
@RunWith
、@Rule
和@ClassRule
替换成**@ExtendWith**。
16. 指标监控
16.1. SpringBoot Actuator
16.1.1. 简介
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务只需快速引用即可获得生产级别的应用监控、审计等功能。
官方文档 - Spring Boot Actuator: Production-ready Features
16.1.2. 1.x与2.x的不同
SpringBoot Actuator 1.x
-
支持SpringMVC
-
基于继承方式进行扩展
-
层级Metrics配置
-
自定义Metrics收集
-
默认较少的安全策略
SpringBoot Actuator 2.x
-
支持SpringMVC、JAX-RS以及Webflux
-
注解驱动进行扩展
-
层级&名称空间Metrics
-
底层使用MicroMeter,强大、便捷默认丰富的安全策略
16.1.3. 使用
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
访问http://localhost:8080/actuator/**
以 HTTP 方式暴露所有监控信息。
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露
下面是一些监控指标:
-
http://localhost:8080/actuator/beans
-
http://localhost:8080/actuator/configprops
-
http://localhost:8080/actuator/metrics
-
http://localhost:8080/actuator/metrics/jvm.gc.pause
-
http://localhost:8080/actuator/metrics/endpointName/detailPath
16.2. Actuator Endpoint
16.2.1. 常用端点
ID | 描述 |
---|---|
auditevents | 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件。 |
beans | 显示应用程序中所有Spring Bean的完整列表。 |
caches | 暴露可用的缓存。 |
conditions | 显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
configprops | 显示所有@ConfigurationProperties。 |
env | 暴露Spring的属性ConfigurableEnvironment |
flyway | 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。 |
health | 显示应用程序运行状况信息。 |
httptrace | 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。 |
info | 显示应用程序信息。 |
integrationgraph | 显示Spring integrationgraph 。需要依赖spring-integration-core。 |
loggers | 显示和修改应用程序中日志的配置。 |
liquibase | 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。 |
metrics | 显示当前应用程序的“指标”信息。 |
mappings | 显示所有@RequestMapping路径列表。 |
scheduledtasks | 显示应用程序中的计划任务。 |
sessions | 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 |
shutdown | 使应用程序正常关闭。默认禁用。 |
startup | 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup。 |
threaddump | 执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring
WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
---|---|
heapdump | 返回hprof堆转储文件。 |
jolokia | 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core。 |
logfile | 返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。 |
prometheus | 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus。 |
其中最常用的Endpoint:
-
Health:监控状况
-
Metrics:运行时指标
-
Loggers:日志记录
16.2.2. Health Endpoint
健康检查端点,一般用于云平台,平台会定时检查应用的健康状况,Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
-
health endpoint返回的结果,是一系列健康检查后的一个汇总报告。
-
很多的健康检查默认已经自动配置好了,比如:数据库、redis等。
-
可以很容易的添加自定义的健康检查机制。
16.2.3. Metrics Endpoint
提供详细的、层级的空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到:
-
通过Metrics对接多种监控系统。
-
简化核心Metrics开发。
-
添加自定义Metrics或者扩展已有Metrics。
16.2.4. 管理Endpoints
16.2.4.1. 开启与禁用Endpoints
默认所有的Endpoint除了shutdown都是开启的。
需要开启或者禁用某个Endpoint。配置模式为management.endpoint.\<endpointName\>.enabled = true
management:
endpoint:
beans:
enabled: true
或者禁用所有的Endpoint然后手动开启指定的Endpoint:
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true
16.2.4.2. 暴露Endpoints
支持的暴露方式:
-
HTTP:默认只暴露health和info。
-
JMX:默认暴露所有Endpoint。
除过health和info,剩下的Endpoint都应该进行保护访问。如果引入Spring
Security,则会默认配置安全访问规则。
ID | JMX | Web |
---|---|---|
auditevents | Yes | No |
beans | Yes | No |
caches | Yes | No |
conditions | Yes | No |
configprops | Yes | No |
env | Yes | No |
flyway | Yes | No |
health | Yes | Yes |
heapdump | N/A | No |
httptrace | Yes | No |
info | Yes | Yes |
integrationgraph | Yes | No |
jolokia | N/A | No |
logfile | N/A | No |
loggers | Yes | No |
liquibase | Yes | No |
metrics | Yes | No |
mappings | Yes | No |
prometheus | N/A | No |
scheduledtasks | Yes | No |
sessions | Yes | No |
shutdown | Yes | No |
startup | Yes | No |
threaddump | Yes | No |
若要更改公开的Endpoint,请配置以下的包含和排除属性:
Property | Default |
---|---|
management.endpoints.jmx.exposure.exclude | |
management.endpoints.jmx.exposure.include | * |
management.endpoints.web.exposure.exclude | |
management.endpoints.web.exposure.include | info, health |
16.3. 定制Endpoint
16.3.1. 定制 Health 信息
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
通过实现HealthIndicator
接口,或继承MyComHealthIndicator
类来自定义。
@Component
public class MyHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
// 构建 health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
return Health.up().build();
}
}
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {
/**
* 真实的检查方法
*
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// mongodb, 获取连接进行测试
Map<String, Object> map = new HashMap<>();
// 检查完成
if (1 == 2) {
// builder.up(); //健康
builder.status(Status.UP);
map.put("count", 1);
map.put("ms", 100);
} else {
// builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err", "连接超时");
map.put("ms", 3000);
}
builder.withDetail("code", 100)
.withDetails(map);
}
}
16.3.2. 定制info信息
常用两种方式
-
编写配置文件
info: appName: boot-admin version: 2.0.1 mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值 mavenProjectVersion: @project.version@
-
编写InfoContributor
@Component public class ExampleInfoContributor implements InfoContributor { @Override public void contribute(Info.Builder builder) { builder.withDetail("example", Collections.singletonMap("key", "value")); } }
然后访问http://localhost:8080/actuator/info,就会输出以上方式返回的所有info信息
16.3.3. 定制Metrics信息
16.3.3.1. SpringBoot支持自动适配的Metrics
-
JVM metrics, report utilization of:
-
Various memory and buffer pools
-
Statistics related to garbage collection
-
Threads utilization
-
Number of classes loaded/unloaded
-
-
CPU metrics
-
File descriptor metrics
-
Kafka consumer and producer metrics
-
Log4j2 metrics: record the number of events logged to Log4j2 at each level
-
Logback metrics: record the number of events logged to Logback at each level
-
Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time
-
Tomcat metrics (server.tomcat.mbeanregistry.enabled must be set to true for all Tomcat metrics to be registered)
-
Spring Integration metrics
16.3.3.2. 定制Metrics
@Component
@Endpoint(id = "myservice")
public class MyServiceEndPoint {
@ReadOperation
public Map getDockerInfo(){
//端点的读操作 http://localhost:8080/actuator/myservice
return Collections.singletonMap("dockerInfo","docker started.....");
}
@WriteOperation
public void stopDocker(){
System.out.println("docker stopped.....");
}
}
也可以使用下面的方式
//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
上面这个方法写到配置类中。
16.4. 定制Endpoint
@Component
@Endpoint(id = "container")
public class DockerEndpoint {
@ReadOperation
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation
private void restartDocker(){
System.out.println("docker restarted....");
}
}
场景:
开发ReadinessEndpoint来管理程序是否就绪,或者是开发LivenessEndpoint来管理程序是否存活。
当然,这个也可以直接使用:https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes
17. Profile
17.1. Profile功能
为了方便多环境适配,Spring Boot简化了profile功能。
17.1.1. application-profile功能
-
默认配置文件
application.yaml
任何时候都会加载。 -
指定环境配置文件
application-{env}.yaml
,env表示绑定的环境名称,比如test、dev、prod等 -
激活指定环境
-
配置文件激活:
spring.profiles.active=prod
,在默认配置文件中指定 -
命令行激活:
java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
- 修改配置文件的任意值,命令行优先
-
-
默认配置与环境配置同时生效
-
同名配置项,profile配置优先
17.1.2. @Profile条件装配功能
加到类上:
@Profile(value = {"prod", "default"})// 加载application-prod.yaml以及application.yaml里的配置
@Component
@ConfigurationProperties("person")
@Data
public class Worker {
private String name;
private Integer age;
}
加到方法上:
@Configuration
public class MyConfig {
@Profile("prod")
@Bean
public Color red() {
return new Color();
}
@Profile("test")
@Bean
public Color green() {
return new Color();
}
}
17.1.3. profile分组
properties:
spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq
spring.profiles.active=production
yaml:
profiles:
active: [ default, test, prod ]
17.2. 外部化配置
官方文档 - Externalized Configuration
17.2.1. 外部配置源
Java属性文件、YAML文件、环境变量、命令行参数
17.2.2. 配置文件查找位置
classpath 根路径。
classpath 根路径下config目录。
jar包当前目录。
jar包当前目录的config目录。
/config 子目录的直接子目录。
17.2.3. 配置文件加载顺序
-
当前jar包内部的application.properties和application.yml。
-
当前jar包内部的application-{profile}.properties 和application-{profile}.yml。
-
引用的外部jar包的application.properties和application.yml。
-
引用的外部jar包的application-{profile}.properties和application-{profile}.yml。
指定环境优先,外部优先,后面的可以覆盖前面的同名配置项。