尚硅谷雷丰阳的SpringBoot零基础入门教程

本视频笔记地址:https://yuque.com/atguigu/springboot

本视频源码地址:https://gitee.com/leifengyang/springboot2

Spring官网: https://spring.io/

SpringBoot2的环境要求

image-20230620092919922

一.SpringBoot2核心技术-基础入门

1.Spring能做什么

这里的Spring指的是整个Spring生态

image-20230620093420150

微服务 响应式编程 分布式 WEB开发 无服务开发 事件驱动 批处理

2.SpringBoot

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.

能快速创建出生产级别的Spring应用

SpringBoot是整合Spring技术栈的一站式框架

SpringBoot是简化Spring技术栈的快速开发脚手架

2.1 SpringBoot的优点

● Create stand-alone Spring applications
○ 创建独立Spring应用
● Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
○ 内嵌web服务器
● Provide opinionated ‘starter’ dependencies to simplify your build configuration
○ 自动starter依赖,简化构建配置
● Automatically configure Spring and 3rd party libraries whenever possible
○ 自动配置Spring以及第三方功能
● Provide production-ready features such as metrics, health checks, and externalized configuration
○ 提供生产级别的监控、健康检查及外部化配置
● Absolutely no code generation and no requirement for XML configuration
○ 无代码生成、无需编写XML

2.2、SpringBoot缺点

  • 人称版本帝,迭代快,需要时刻关注变化(说明社区活跃,也是个优点)
  • 封装太深,内部原理复杂,不容易精通

2.3 官方文档

进入官网 找到SpringBoot 点击Reference Doc进入官方文档

image-20230620101533445

3.SpringBoot入门

1.系统的要求

  • Java 8 & 兼容java14 .
  • Maven 3.3+
  • idea 2019.1.2

**Maven的配置教程: **

Maven配置文件settings.xml | The Blog (qingling.icu)

开发环境的搭建 | The Blog (qingling.icu)

2.HelloWorld

需求:浏览发送/hello请求,响应 Hello,Spring Boot 2

1创建maven工程

image-20230725160846882

2.引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<!-- WEB场景的启动器 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

</dependencies>

3.创建主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.atguigu.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author Jason Gong
* @version 1.0
* @website https://qingling.icu
* @Date 2023/7/25
* @Description
*/
@SpringBootApplication //标注这是一个SpringBoot应用
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}

4.编写Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.atguigu.boot.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author Jason Gong
* @version 1.0
* @website https://qingling.icu
* @Date 2023/7/25
* @Description
*/
@RestController
public class HelloController {

@RequestMapping
public String handle01(){
return "Hello,Spring Boot2";
}
}
image-20230725162201612

3.简化配置

官网地址:https://docs.spring.io/spring-boot/docs/2.7.14/reference/html/application-properties.html#appendix.application-properties

1
server.port=8888

4.简化部署

通过在pom.xml文件中插入这个插件,可以直接将项目打成jar包简化部署

1
2
3
4
5
6
7
8
 <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

image-20230725163026091

可以直接使用cmd切换到target目录下使用java -jar 命令运行jar包

image-20230725163212347

注意点

去掉cmd的快速编辑模式(不去掉启动的时候点击屏幕会停止)

image-20230725164124574

4.了解自动装配原理

4.1 依赖管理

1.SpringBoot的自动版本仲裁机制

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 项目的继承的父项目-->
<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>

image-20230725165414051

自定义mysql版本驱动

在父依赖中mysql是8版本的

image-20230725170329032

可以在pom.xml中通过自定义properties修改版本

1
2
3
4
<properties>
<!--重写mysql的版本号-->
<mysql.version>5.1.43</mysql.version>
</properties>

image-20230725170257030

2.Starters场景启动器

官网: https://docs.spring.io/spring-boot/docs/2.7.14/reference/html/using.html#using.build-systems.starters

spring-boot-starter-*起头的starter为官方提供的starter,*-spring-boot-starter为第三方提供的starter

常见的Starter

image-20230725171130925

以引入spring-boot-starter-web为例,引入该starter它会帮我们引入以下的其它依赖

image-20230725171550618

4.2 自动配置

  • 自动配置Tomcat

    1. 引入依赖

      image-20230725172719608

    2. 配置Tomcat

  • 自动配置SpringMvc

    1. 引入springMvc全套组件

    2. 自动配置好springMvc的常用组件(字符编码,文件上传解析器,dispatcherServlet等)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @SpringBootApplication
      public class MainApplication {
      public static void main(String[] args) {
      //返回ioc容器
      ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
      //获取容器里面组件的名字
      String[] beanDefinitionNames = run.getBeanDefinitionNames();
      for (String beanDefinitionName : beanDefinitionNames) {
      //打印容器内的组件
      System.out.println(beanDefinitionName);
      }
      }
      }

      image-20230725172834666

  • 默认的包结构

    主程序所在的包以及其的子包都可以被扫描到,无需配置包扫描

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    com
    +- example
    +- myapplication
    +- MyApplication.java #主程序
    |
    +- customer
    | +- Customer.java
    | +- CustomerController.java
    | +- CustomerService.java
    | +- CustomerRepository.java
    |
    +- order
    +- Order.java
    +- OrderController.java
    +- OrderService.java
    +- OrderRepository.java

    如果需要扫描的文件在主程序的上级目录,我们也想扫描到它,我们需要扩大一下包的扫描范围

    1
    2
    3
    @SpringBootApplication(scanBasePackages = "com.atguigu")
    或者
    @ComponentScan("com.atguigu")
  • 各种配置拥有默认值

    image-20230725174350129

  • 按需加载所有的自动配置项

    spingboot的所有配置存在于下面的这个依赖中

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.3.4.RELEASE</version>
    <scope>compile</scope>
    </dependency>

    image-20230725175010683

4.3 容器功能

1.组件添加

1.1 @Configuration
  • 基本使用

  • Full模式与Lite模式

    • 示例
    • 最佳实战
      • 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
      • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 1、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的
* 2、配置类本身也是组件
* 3、proxyBeanMethods:代理bean的方法
* Full(proxyBeanMethods = true)、【保证每个@Bean方法被调用多少次返回的组件都是单实例的】
* Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】
* 组件依赖必须使用Full模式默认。其他默认是否Lite模式
*/
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {

/**
* Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
* @return
*/
@Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
public User user01(){
User zhangsan = new User("zhangsan", 18);
//user组件依赖了Pet组件
zhangsan.setPet(tomcatPet());
return zhangsan;
}

@Bean("tom")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@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总会检查这个组件是否在容器中有。
//保持组件单实例
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));
}
}
1.2 @Bean @Component @Controller @Service @Repository
1
2
3
4
5
//默认是单实例的
@Bean//给容器中添加组件,以方法名作为组件的id(组件名),返回类型就是组件类型,返回的值,就是组件在容器中的实例,@Bean("user") 是自定义组件名为user
public User user01(){
return new User("Tom",100);
}
1.3 @ComponentScan @Import
1
2
3
4
5
6
7
/*
* @Import({User.class, DBHelper.class}) 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
*/
@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false)
public class MyConfig {
}
1.4 @Conditional 条件装配:满足Conditional指定的条件,则进行组件注入

image-20230802141422751

1
2
3
//放在类上面,条件成立,这个类下面的所有配置生效,放在方法上面,条件成立,这个方法下的配置才会成立
@ConditionalOnBean(name = "tom") //存在一个id为tom的组件的时候才会生效
@ConditionalOnMissingBean(name = "tom")

2.原生配置文件的引入

2.1、@ImportResource

beans.xml*)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<bean id="haha" class="com.atguigu.boot.bean.User">
<property name="name" value="zhangsan"></property>
<property name="age" value="18"></property>
</bean>

<bean id="hehe" class="com.atguigu.boot.bean.Pet">
<property name="name" value="tomcat"></property>
</bean>
</beans>

在java类中红使用@ImportResource(“classpath:beans.xml”)导入上面的配置文件

1
2
@ImportResource("classpath:beans.xml")
public class MyConfig {}

3.配置绑定

3.1 @ConfigurationProperties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 只有在容器中的组件,才会拥有SpringBoot提供的强大功能
*/
@Component //或者在配置类上添加@EnableConfigurationProperties(Car.class)注解,不使用@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {

private String brand;
private Integer price;

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public Integer getPrice() {
return price;
}

public void setPrice(Integer price) {
this.price = price;
}

@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", price=" + price +
'}';
}
}

配置文件中的值

1
2
mycar.brand=BYD
mycar.price=120000

编写测试的controller

1
2
3
4
5
6
@Autowired
private Car car;
@RequestMapping("/car")
public Car ggetCar(){
return car;
}
image-20230802153951542

4.4 自动配置原理入门

1.引导加载自动配置类

@SpringBootApplication

image-20230802155222746

1
2
3
4
5
@SpringBootConfiguration //代表当前是一个配置类
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })//指定扫描那些spring注解
public @interface SpringBootApplication{}

@EnableAutoConfiguration

1
2
3
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}

@AutoConfigurationPackage

1
2
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {}

Registrar.class

image-20230802160618158

利用Registrar给容器中导入com.atguigu.boot(主程序所在的包)包下的一系列组件

image-20230802161004735

@Import(AutoConfigurationImportSelector.class)

1
2
3
4
5
6
1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
2、调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件
4、从META-INF/spring.factories位置来加载一个文件。
默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

AutoConfigurationImportSelector.java

1
2
3
4
5
6
7
8
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

getAutoConfigurationEntry()方法

利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}

**getCandidateConfigurations()**获取所有候选的配置

调用List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类

image-20230802162236987

loadSpringFactories()

利用工厂加载 Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
MultiValueMap<String, String> result = new LinkedMultiValueMap();

while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();

while(var6.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;

for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}

cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}

从META-INF/spring.factories位置来加载一个文件。默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件,spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

image-20230802163429366

在spring.factories这个配置文件中写死了springBoot一启动就要给容器加载的所有配置类,一共127个

image-20230802163708198

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\
org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer

# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider

2.按需开启自动配置项

​ 虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration,按照条件装配规则(@Conditional),最终会按需配置,比如下面的配置是否生效还要通过@Conditional的条件判断。

简单的一句话:启动的时候加载所有,使用的时候按照条件(@Conditional)进行装配

image-20230802164424689

3.修改默认配置

​ 相当于当我们在容器中注入了类型为MultipartResolver但是id不为multipartResolver的组件的时候,会帮我们规范一下命名,命名为multipartResolver。

1
2
3
4
5
6
7
@Bean
@ConditionalOnBean(MultipartResolver.class) //存在MultipartResolver类型的组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)//不存在以multipartResolver为id的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}

SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先

1
2
3
4
5
6
7
8
9
@Bean
@ConditionalOnMissingBean //没有的话就将这个配置注入到容器中
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
return filter;
}

总结:

  • SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定

  • 生效的配置类就会给容器中装配很多组件

  • 只要容器中有这些组件,相当于这些功能就有了

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改。

xxxxxAutoConfiguration —> 组件 —> xxxxProperties取值 —-> application.properties

3.4.最佳实践

4.5 简化开发

1.Lombok

引入依赖(使用前需要安装插件)

1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

注解

1
2
3
4
5
@NoArgsConstructor  //无参构造器
@AllArgsConstructor //有参构造器
@Data //getter和setter方法
@ToString
@EqualsAndHashCode

2.简化日志开发

1
@Slf4j

3.dev-tools

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

项目或者页面修改以后:Ctrl+F9;

4.Spring Initailizr(项目初始化向导)

image-20230803103657995

image-20230803104032505

image-20230803104655578

二.SpringBoot2核心技术-核心功能

image-20230803104919434

一.配置文件

1、文件类型

1.1、properties

同以前的properties用法

1.2、yaml

1.2.1、简介

YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:”Yet Another Markup Language”(仍是一种标记语言)。

非常适合用来做以数据为中心的配置文件

1.2.2、基本语法
  • key: value;kv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • ‘#’表示注释
  • 字符串无需加引号,如果要加,’’与””表示字符串内容 会被 转义/不转义
1.2.3、数据类型
  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
1
k: v
  • 对象:键值对的集合。map、hash、set、object
1
2
3
4
5
6
行内写法:  k: {k1:v1,k2:v2,k3:v3}
#或
k:
k1: v1
k2: v2
k3: v3
  • 数组:一组按次序排列的值。array、list、queue
1
2
3
4
5
6
行内写法:  k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
1.2.4、示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ConfigurationProperties(prefix="person")//和以person开头的配置文件绑定
@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;
}

@Data
public class Pet {
private String name;
private Double weight;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# yaml表示以上对象
person:
# 单引号会将 \n作为字符串输出 双引号会将\n 作为换行输出
# 双引号不会转义,单引号会转义
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,weight: 47}
health: [{name: mario,weight: 47}]

2、配置提示

自定义的类和配置文件绑定一般没有提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

二.Web开发

image-20230803134143859

1.简单的功能分析

1.1 静态资源访问

1.静态资源目录

只要静态资源放在类路径下: /static (or /public or /resources or /META-INF/resources

访问 : 当前项目根路径/ + 静态资源名

原理: 静态映射/**

请求进来,先去找Controller看能不能处理,不能处理的所有请求又都交给静态资源处理器,静态资源也找不到则响应404页面

改变默认的静态资源路径

1
2
3
spring:
resources:
static-locations: [classpath:/haha/] #静态资源的目录
2.静态资源访问前缀

默认无前缀

默认的访问路径: localhost:8080/xxx.png

设置访问前缀之后: localhost:8080/res/xxx.png (文件在目录中的位置没有改变,只是访问的时候加上了res这一层路径)

1
2
3
spring:
mvc:
static-path-pattern: /res/**

当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找

3.webjar

自动映射 /webjars/**

https://www.webjars.org/

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径

1.2 欢迎页支持

  • 静态资源路径下 index.html

    • 可以配置静态资源路径
    • 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
1
2
3
4
5
6
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效

resources:
static-locations: [classpath:/haha/]
  • controller能处理/index

1.3 自定义 Favicon

favicon.ico 放在静态资源目录下即可(名称为favicon.ico)

1
2
3
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效

1.4 静态资源配置原理

1.SpringMvc的自动配置类WebMvcAutoConfiguration

image-20230803165113057

1
2
3
4
5
6
7
8
@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 {}

2.SpringMvc给容器中配置的组件

image-20230803180257123

1
2
3
4
5
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}

3.绑定的配置

@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })

WebMvcProperties.class

image-20230803180600438

ResourceProperties.class

image-20230803180633611

2.请求参数处理与数据响应

2.1、请求映射

1、rest使用与原理
  • @xxxMapping;

  • Rest风格支持(使用HTTP请求方式动词来表示对资源的操作

    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
    • 核心Filter;HiddenHttpMethodFilter
      • 用法: 表单method=post,隐藏域 _method=put
      • SpringBoot中手动开启
      1
      2
      3
      4
      5
      6
      #开启使用rest风格的注解
      spring:
      mvc:
      hiddenmethod:
      filter:
      enabled: true
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <!--发送put或者delete请求-->
      <form action="/user" method="post">
      <input name="_method" type="hidden" value="delete"/>
      <input name="_m" type="hidden" value="delete"/>
      <input value="REST-DELETE 提交" type="submit"/>
      </form>
      <form action="/user" method="post">
      <input name="_method" type="hidden" value="PUT"/>
      <input value="REST-PUT 提交" type="submit"/>
      </form>
    • 扩展:如何把_method 这个名字换成我们自己喜欢的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-张三";
}

@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}


@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-张三";
}

@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}



//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}

Rest原理(表单提交要使用REST的时候)

  • 表单提交会带上_method=PUT

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--发送put或者delete请求-->
    <form action="/user" method="post">
    <input name="_method" type="hidden" value="delete"/>
    <input name="_m" type="hidden" value="delete"/>
    <input value="REST-DELETE 提交" type="submit"/>
    </form>
    <form action="/user" method="post">
    <input name="_method" type="hidden" value="PUT"/>
    <input value="REST-PUT 提交" type="submit"/>
    </form>
  • 请求过来被HiddenHttpMethodFilter拦截

    WebMvcAutoConfiguration

    1
    2
    3
    4
    5
    6
    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
    }
    • 请求是否正常,并且是POST
      • 获取到_method的值。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        HttpServletRequest requestToUse = request;

        if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        String paramValue = request.getParameter(this.methodParam);
        if (StringUtils.hasLength(paramValue)) {
        String method = paramValue.toUpperCase(Locale.ENGLISH);
        if (ALLOWED_METHODS.contains(method)) {
        requestToUse = new HttpMethodRequestWrapper(request, method);
        }
        }
        }

        filterChain.doFilter(requestToUse, response);
        }
      • 兼容以下请求;PUT.DELETE.PATCH

        1
        2
        3
        private static final List<String> ALLOWED_METHODS =
        Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
        HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
      • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值

      • 过滤器链放行的时候用wrapper,以后的方法调用getMethod是调用requesWrapper的

Tips:

Rest使用客户端工具:如PostMan直接发送put、delete等方式请求,无需Filter,不需要转换

扩展点:

如何将<input name="_method" type="hidden" value="delete"/>中的name值_method改为自定义的值?

1
2
3
4
5
6
7
8
9
@Configuration(proxyBeanMethods = false) //组件中没有依赖关系,设置成false直接放
public class WebConfig{
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m"); //将name="_method"改成name="_m"
return methodFilter;
}
}
2、请求映射原理

DispatcherServlet 继承 FrameworkServlet

FrameworkServlet 继承 HttpServletBean

FrameworkServlet 重写了doGet和doPost方法

image-20230809133846464

DispatcherServlet的doDispatch方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
//找到请求使用那个hander(contrller)处理
/**
* getHandler()方法
*/
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

getHandler()方法 遍历查找可以处理请求的方法

1
2
3
4
5
6
7
8
9
10
11
12
@Nullable
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;
}

**HandlerMapping ** 处理器映射

默认有5个HandlerMapping

image-20230809150915450

RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则

image-20230809151427867

所有的请求映射都在HandlerMapping中

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 访问 /能访问到index.html;

  • SpringBoot自动配置了默认的 RequestMappingHandlerMapping

  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息

    • 如果有就找到这个请求对应的handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping 自定义 HandlerMapping

2.2、普通参数与基本注解

1.注解
1
2
3
4
5
6
7
@PathVariable  //获取路径中的参数信息(restful风格)
@RequestHeader //获取请求头的信息
@RequestAttribute //获取请求域中的数据
@RequestParam //获取路径中拼接的请求参数(/info?age=12&name=lisi)
@MatrixVariable //矩阵变量 /cars/sell;low=34;brand=byd,audi,yd
@CookieValue //获取cookie的值
@RequestBody //获取请求体的值(Post请求)

@PathVariable

接受路径中的参数也可以使用map集合接受,但是map集合必须是Map<String, String>的形式

image-20230809155753715

@RequestHeader

获取指定的请求头或者所有的请求头信息

image-20230809160406516

image-20230809160846527

@RequestParam

可以使用map接受所有的请求参数

image-20230809161533834

@CookieValue

获取cookie的值,可以直接获取字符串的值也可以封装成cookie的对象

image-20230809164101374

@RequestBody

这里用一个字符串接收表单数据和平时使用的不一样,开发中使用一个对象接受表单的数据

1
2
3
4
5
6
7
8
9
/**
* ·@RequestBody注解
*/
@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}

image-20230809165149278

@RequestAttribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.atguigu.boot.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
* @author Jason Gong
* @version 1.0
* @website https://qingling.icu
* @Date 2023/8/11
* @Description
*/
@Controller
public class RequestController {


@GetMapping("/goto")
public String goToPage(HttpServletRequest request){

request.setAttribute("msg","获取请求域中的数据成功");
request.setAttribute("code",200);
return "forward:/success"; //转发到下面的/success请求
}

@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
@RequestAttribute(value = "code",required = false)Integer code,//通过注解的方式获取上面请求域的值
HttpServletRequest request){//通过原生的request获取
//Object msg1 = request.getAttribute("msg");
Map<String,Object> map = new HashMap<>();
map.put("msg",msg);
map.put("code",code);
return map;
}
}

image-20230811092618793

@MatrixVariable

开启SpringBoot的矩阵变量功能

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除 后面的内容 矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
//2、SpringBoot默认是禁用了矩阵变量的功能
// 手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
// removeSemicolonContent(移除分号内容)支持矩阵变量的
//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;
}

image-20230811095858532

image-20230811095834803

2.Servlet API

WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId

ServletRequestMethodArgumentResolver 以上的部分参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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.复杂参数:

Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

向请求域中共享数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* ra数据通过 map model HttpServletRequest HttpServletResponse 等方式共享到请求域中,在success页面中取出来
*/
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
map.put("hello","world666");
model.addAttribute("world","hello666");
request.setAttribute("message","HelloWorld");

Cookie cookie = new Cookie("c1","v1");
response.addCookie(cookie);
return "forward:/success";
}
4.自定义对象参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {

private String userName;
private Integer age;
private Date birth;
private Pet pet;

}

@Data
public class Pet {

private String name;
private String age;

}

页面测试代码

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>atguigu,欢迎您</h1>
测试REST风格;
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input value="REST-POST 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="delete"/>
<input name="_m" type="hidden" value="delete"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT"/>
<input value="REST-PUT 提交" type="submit"/>
</form>
<hr/>
测试基本注解:
<ul>
<a href="car/3/owner/lisi?age=18&inters=basketball&inters=game">car/{id}/owner/{username}</a>
<li>@PathVariable(路径变量)</li>
<li>@RequestHeader(获取请求头)</li>
<li>@RequestParam(获取请求参数)</li>
<li>@CookieValue(获取cookie值)</li>
<li>@RequestBody(获取请求体[POST])</li>

<li>@RequestAttribute(获取request域属性)</li>
<li>@MatrixVariable(矩阵变量)</li>
</ul>

/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;<br/>
/cars/sell;low=34;brand=byd,audi,yd ;矩阵变量 <br/>
页面开发,cookie禁用了,session里面的内容怎么使用;
session.set(a,b)---> jsessionid ---> cookie ----> 每次发请求携带。
url重写:/abc;jsesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递.

/boss/1/2

/boss/1;age=20/2;age=20

<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a>
<a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)</a>
<a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}</a>
<br/>
<form action="/save" method="post">
测试@RequestBody获取数据 <br/>
用户名:<input name="userName"/> <br>
邮箱:<input name="email"/>
<input type="submit" value="提交"/>
</form>
<ol>
<li>矩阵变量需要在SpringBoot中手动开启</li>
<li>根据RFC3986的规范,矩阵变量应当绑定在路径变量中!</li>
<li>若是有多个矩阵变量,应当使用英文符号;进行分隔。</li>
<li>若是一个矩阵变量有多个值,应当使用英文符号,进行分隔,或之命名多个重复的key即可。</li>
<li>如:/cars/sell;low=34;brand=byd,audi,yd</li>
</ol>
<hr/>
测试原生API:
<a href="/testapi">测试原生API</a>
<hr/>
测试复杂类型:<hr/>
测试封装POJO;
<form action="/saveuser" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
<!-- 宠物姓名:<input name="pet.name" value="阿猫"/><br/>-->
<!-- 宠物年龄:<input name="pet.age" value="5"/>-->
宠物: <input name="pet" value="小猫"/>
<input type="submit" value="保存"/>
</form>
<br>
</body>
</html>

2.3 参数处理原理

1.页面发送请求

1
http://localhost:8080/car/3/owner/lisi  

2.进入DispatcherServlet的doDispatch( )方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

3.封装一个方法获取对应的处理器适配器

1
2
//方法的调用
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

4.遍历所有的处理器适配器(一共有四种,如下图)找到支持的适配器

1
2
3
4
5
6
7
8
9
10
11
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

image-20230812091950114

上图中四个处理器适配器中前两个是常用的处理器适配器: 0 - 支持方法上标注@RequestMapping 1 - 支持函数式编程的

5.查看当前的处理器适配器是否支持的方法

1
2
3
4
5
6
@Override
public final boolean supports(Object handler) {
//条件一 handler instanceof HandlerMethod
//条件二 supportsInternal((HandlerMethod) handler)
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}

遍历查找之后返回的适配器是RequestMappingHandlerAdapter

image-20230812092825385

6.回到DispatcherServlet执行下面的方法

1
2
3
//根据上面获取到的处理器适配器真正的执行处理器方法
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

7.内部的处理过程

handle( )方法

1
2
3
4
5
6
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}

进入当前处理器适配器的handleInternal( )方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ModelAndView mav;
checkRequest(request);

// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}

if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}

return mav;
}

执行invokeHandlerMethod( )方法

1
mav = invokeHandlerMethod(request, response, handlerMethod);

进入invokeHandlerMethod()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}

invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}

return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}

注意这里的26个参数解析器,参数解析器的作用是确定将要执行的目标方法的每一个参数的值是什么

这里的每一个参数解析器都对应了我们标注在参数上的注解

SpringMVC目标方法能写多少种参数类型取决于参数解析器

image-20230812094224699

参数解析器的接口设计

image-20230812094755583

  • 当前解析器是否支持解析这种参数
  • 支持就调用 resolveArgument

返回值处理器

定义了controller返回值的类型

image-20230812095137195

RequestMappingHandlerAdapter的invokeAndHandle( )方法真正的执行请求

1
invocableMethod.invokeAndHandle(webRequest, mavContainer);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//上面一行代码执行完毕之后会直接跳到controller方法,然后执行下面的代码
setResponseStatus(webRequest);

if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}

mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}

执行的过程

1
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
1
2
3
4
5
6
7
8
9
10
11
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
//这个args就记录了方法上所有的参数
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}

image-20230812100059378

获取方法参数值的过程

1
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {


//获取所有参数的参数声明 参数类型 标注的注解
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}

Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}

遍历判断获取支持解析该参数的参数解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}

解析参数的值通过调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法

2.4 数据响应与内容协商

返回值的处理逻辑

2.4.1 相关依赖的引入

创建springBoot项目的时候导入了spring-boot-starter-web依赖会自动的帮我们导入相关的json依赖,便于json数据的前后端传递

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

自动的帮我们导入相关的json的starter

1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>

在spring-boot-starter-json中引入了jackson相关的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>

返回值解析器的原理

  • 1、返回值处理器判断是否支持这种类型返回值 supportsReturnType

  • 2、返回值处理器调用 handleReturnValue 进行处理

  • 3、RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。

      1. 利用 MessageConverters 进行处理 将数据写为json
      • 1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
      • 2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
      • 3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
        • 1、得到MappingJackson2HttpMessageConverter可以将对象写为json
        • 2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
2.4.2 内容协商原理

引入jackson-dataformat-xml,测试返回是xml格式的数据

1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

开启基于请求参数的内容协商功能

发请求: http://localhost:8080/test/person?format=json //指定格式是json

http://localhost:8080/test/person?format=xml //指定格式是xml

1
2
3
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式

1、Parameter策略优先确定是要返回json数据(获取请求头中的format的值)

2、最终进行内容协商返回给客户端json即可

3、遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)

4、找到支持操作Person的converter,把converter支持的媒体类型统计出来。

5、客户端需要【application/xml】。服务端能力【10种、json、xml】

6、进行内容协商的最佳匹配媒体类型

7、用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。

2.4.3 自定义MessageConverter

1、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理

2、Processor 处理方法返回值。通过 MessageConverter 处理

3、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)

4、内容协商找到最终的 messageConverter;

步骤:
1、添加自定义的MessageConverter进系统底层

2、系统底层就会统计出所有MessageConverter能操作哪些类型

3、客户端内容协商 [guigu—>guigu]

编写自定义的的converter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.atguigu.boot.converter;

import com.atguigu.boot.bean.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

/**
* 自定义的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());
}
}

在配置文件中加入自定义的converter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.atguigu.boot.config;


import com.atguigu.boot.bean.Person;
import com.atguigu.boot.bean.Pet;
import com.atguigu.boot.converter.GuiguMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.StringUtils;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;

import java.io.IOException;
import java.io.OutputStream;
import java.util.*;

@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
//加入自定义的converter
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
};
}
}

在浏览器url上拼接参数的形式实现内容协商

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.atguigu.boot.config;

import com.atguigu.boot.bean.Person;
import com.atguigu.boot.bean.Pet;
import com.atguigu.boot.converter.GuiguMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.StringUtils;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;
import java.io.IOException;
import java.io.OutputStream;
import java.util.*;

@Configuration(proxyBeanMethods = false)
public class WebConfig{

@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {

/**
* 自定义内容协商策略
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json",MediaType.APPLICATION_JSON);
mediaTypes.put("xml",MediaType.APPLICATION_XML);
//在浏览器url上拼接参数的形式实现内容协商
//http://localhost:8080/test/person?format=gg
mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu"));
//指定支持解析哪些参数对应的哪些媒体类型
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
//parameterStrategy.setParameterName("ff");

HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy();

configurer.strategies(Arrays.asList(parameterStrategy,headeStrategy));
}

//加入自定义的converter
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
};
}
}

3.视图解析与模板引擎

视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染

3.1 视图解析

image-20230815091741019

3.2 模板引擎-Thymeleaf

详细的使用教程: https://qingling.icu/posts/54835.html

3.2.1 Thymeleaf的使用

1.引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2.编写测试controller

1
2
3
4
5
6
7
8
9
10
@Controller
public class ViewTestController {
@GetMapping("/view")
public String hello(Model model){
//向请求域中共享数据
model.addAttribute("keys","Thymeleaf");
//转发到success页面中
return "success";
}
}

3.页面上渲染

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${keys}">Hello</h1>
</body>
</html>

image-20230815153318041

3.3 后台管理系统注意点

templates目录的访问规则

templates目录下的所有页面资源只能通过请求访问到

表单重复提交的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.atguigu.admin.controller;


@Slf4j
@Controller
public class IndexController {

/**
* 来登录页
* @return
*/
@GetMapping(value = {"/","/login"})
public String loginPage(){

return "login";
}


@PostMapping("/login")
public String main(String username, String password){
//登录成功重定向到main.html; 重定向防止表单重复提交
//这里如果直接写 return "main"的话,页面在mian.html的情况下,url路径还是localhost:8080/login,如果再次刷新页面会重复提交表单
//使用下面这个写法,执行"/login"这个请求之后,路径的url会变成localhost:8080/main.html
return "redirect:/main.html";
}

/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(){
return "main";
}
}

4.拦截器

1.HandlerInterceptor接口

自定义拦截器需要实现的接口,以及需要实现接口中的方法

image-20230821105529796

2.拦截器实现登录检查操作

登录检查的业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);

//登录检查逻辑
HttpSession session = request.getSession();

Object loginUser = session.getAttribute("loginUser");

if(loginUser != null){
//放行
return true;
}

//拦截住 未登录 跳转到登录页
request.setAttribute("msg","请先登录");
request.getRequestDispatcher("/").forward(request,response);
return false;
}

/**
* 目标方法执行完成以后
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}

/**
* 页面渲染以后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}

拦截器的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则[如果是拦截所有,静态资源也会被拦截]
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
}
}

3.拦截器原理

1
2
3
4
5
6
/**
* preHandle() :该方法在控制器方法之前执行,如果返回false,说明拦截器不放行该请求,自己处理或者结束该请求
* 如果返回true,说明拦截器放该请求,由其它匹配的拦截器或者控制器继续处理
* postHandle():该方法在控制器方法调用之后,且解析视图之前执行,可以通过此方法对请求域中的模型和视图做出进一步的修改
* afterCompletion(): 该方法会在整个请求完成,即视图渲染结束之后执行,多用于资源的清理工作
*/

1、根据当前请求,找到HandlerExecutionChain[可以处理请求的handler以及handler的所有 拦截器]

2、先来顺序执行 所有拦截器的 preHandle方法

  • 1、如果当前拦截器prehandler返回为true,则执行下一个拦截器的preHandle
  • 2、如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的afterCompletion方法

3、如果任何一个拦截器返回false,直接跳出不执行目标方法

4、所有拦截器都返回true,执行目标方法

5、倒序执行所有拦截器的postHandle方法

6、前面的步骤有任何异常都会直接倒序触发 afterCompletion

7、页面成功渲染完成以后,也会倒序触发 afterCompletion

image-20230821112246355

5.文件上传

1.页面表单

1
2
3
4
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>

2.文件上传代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* MultipartFile 自动封装上传过来的文件
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,//单个文件
@RequestPart("photos") MultipartFile[] photos) throws IOException {//多个文件

log.info("上传的信息:email={},username={},headerImg={},photos={}",
email, username, headerImg.getSize(), photos.length);

if (!headerImg.isEmpty()) {
//保存到文件服务器,OSS服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("H:\\cache\\" + originalFilename));
}

if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("H:\\cache\\" + originalFilename));
}
}
}
return "main";
}

3.MultipartAutoConfiguration

1
2
3
4
5
6
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {}

6.异常处理

开发中异常处理方法: https://qingling.icu/posts/31385.html

1.默认规则

  • 默认情况下,Spring Boot提供/error处理所有错误的映射
  • 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据

2.自定义错误页面

templates/error/下的4xx,5xx页面会被自动解析

7.Web原生组件注入(Servlet、Filter、Listener)

7.1 使用Servlet Api

1.原生的Servlet的使用

1.编写自定义的servlet继承HttpServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.atguigu.boot.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* @author Jason Gong
* @version 1.0
* @website https://qingling.icu
* @Date 2023/8/26
* @Description
*/
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("6666");
}
}

2.在启动类上添加自定义servlet的包扫描注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan("com.atguigu.boot") //指定原生的servlet的位置
@SpringBootApplication
public class Boot05Web01Application {

public static void main(String[] args) {
SpringApplication.run(Boot05Web01Application.class, args);
}

}

3.启动程序测试

image-20230826211541424

2.原生的Filter的使用

1.编写自定义的Filter实现Filter接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.atguigu.boot.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
* @author Jason Gong
* @version 1.0
* @website https://qingling.icu
* @Date 2023/8/26
* @Description
*/
@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销毁");
}
}

2.启动程序测试

image-20230826212400849

3.原生的Listener的使用

1.编写自定义的MyServletContextListener实现ServletContextListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.atguigu.boot.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

/**
* @author Jason Gong
* @version 1.0
* @website https://qingling.icu
* @Date 2023/8/26
* @Description
*/
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {


@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化完成");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁");
}
}

2.启动程序测试

image-20230826213125490

7.2 使用RegistrationBean

不使用@WebServlet、@WebFilter、@WebListener注解的方式(注释掉自定义servlet、Filter、Listener上的注解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.atguigu.boot.servlet;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;

/**
* @author Jason Gong
* @version 1.0
* @website https://qingling.icu
* @Date 2023/8/26
* @Description
*/
@Configuration
public class MyRegisterConfig {
@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 mySwervletContextListener = new MyServletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}

重新启动项目我们可以看到依然生效

image-20230826214253187

8.嵌入式Servlet容器

8.1 切换嵌入式Servlet容器

  • 默认支持的webServer

    • Tomcat, Jetty, or Undertow
    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
  • 切换服务器

    1.排除tomcat的依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <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>

    2.引入需要的服务器场景

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>

    3.重启项目,可以看到服务器由tomcat变成了undertow

    image-20230826221229898

  • 原理

    • SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
    • web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
    • ServletWebServerApplicationContext 启动的时候寻找 **ServletWebServerFactory**``(Servlet 的web服务器工厂---> Servlet 的web服务器)
    • SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
    • 底层直接会有一个自动配置类ServletWebServerFactoryAutoConfiguration
    • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
    • ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
    • TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
    • 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)

8.2 定制Servlet容器

修改配置文件中以server.xxx打头的属性信息

image-20230826222050817

三.数据访问

1.SQL

1.1 JDBC的使用

1.导入JDBC的场景

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

2.导入自己使用的数据库的驱动(Mysql或者Oracle)版本号不写也可以,官方做了版本仲裁(但是要与实际安装的版本对应)

1
2
3
4
5
6
7
8
9
10
11
12
13
想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、重新声明版本(maven的属性的就近优先原则)
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.45</mysql.version>
</properties>
或者
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>

3.添加配置

1
2
3
4
5
6
7
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
# type: com.zaxxer.hikari.HikariDataSource #配置数据源

4.测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.atguigu.boot;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

@Slf4j
@SpringBootTest
class Boot05Web01ApplicationTests {

@Autowired
JdbcTemplate jdbcTemplate;

@Test
void contextLoads() {
Long count = jdbcTemplate.queryForObject("select count(*) from `role` ", Long.class);
log.info("记录数:{}",count);
}
}

1.2 分析自动配置

自动配置的类

image-20230826224503510

  • DataSourceAutoConfiguration : 数据源的自动配置

    • 修改数据源相关的配置:spring.datasource
    • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
    • 底层配置好的连接池是:HikariDataSource
  • DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置

  • JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud

    • 可以修改这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) 来修改JdbcTemplate
  • JndiDataSourceAutoConfiguration: jndi的自动配置

  • XADataSourceAutoConfiguration: 分布式事务相关的

四.单元测试

1.JUnit5的变化

Spring Boot 2.2.0 版本开始引入 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)

以前的版本

兼容以前的版本(junit4)需要额外导入的依赖(SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<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>

标注的注解

@SpringBootTest + @RunWith(SpringRunner.class)+@Test(4版本的@Test)

现在的版本

需要导入的依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

标注的注解

@SpringBootTest + @Test

两个不同版本的 @Test 注解

image-20230827112742347

2.JUnit5常用注解

JUnit5的注解与JUnit4的注解有所变化

https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

  • @Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
  • @RepeatedTest :表示方法可重复执行,下方会有详细介绍
  • @DisplayName :为测试类或者测试方法设置展示名称

image-20230827114256749

  • @BeforeEach :表示在每个单元测试之前执行

  • @AfterEach :表示在每个单元测试之后执行

  • @BeforeAll :表示在所有单元测试之前执行

  • @AfterAll :表示在所有单元测试之后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.atguigu.boot;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

/**
* @author Jason Gong
* @version 1.0
* @website https://qingling.icu
* @Date 2023/8/27
* @Description 测试junit5的相关注解
*/
@Slf4j
@DisplayName("Junit5Test测试类")
@SpringBootTest
public class Junit5Test {
/**
* @DisplayName 加在方法或者类上,为测试类或者测试方法设置展示名称
*/
@Disabled
@DisplayName("测试@DisplayName注解")
@Test
void testDisplayName(){
log.info("@DisplayName注解的测试");
}

/**
* 在执行的单元测试方法之前运行
*/
@BeforeEach
void testBeforeEach(){
log.info("执行了testBeforeEach()方法");
}

/**
* 在执行的单元测试方法之后运行
*/
@AfterEach
void testAfterEach() {
log.info("执行了testAfterEach()方法");
}

/**
* 在所有的单元测试执行之后执行
* 注意是静态方法
*/
@BeforeAll
static void testBeforeAll() {
log.info("执行了testBeforeAll()方法");
}

/**
* 在所有的单元测试执行之前执行
* 注意是静态方法
*/
@AfterAll
static void testAfterAll() {
log.info("执行了testAfterAll()方法");
}
}
  • @Tag :表示单元测试类别,类似于JUnit4中的@Categories
  • @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore

image-20230827115900097

  • @Timeout :表示测试方法运行如果超过了指定时间将会返回错误
1
2
3
4
5
6
7
8
/**
* 规定方法超时时间 超出时间测试出异常
*/
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}
  • @ExtendWith :为测试类或测试方法提供扩展类引用
1
2
3
4
5
6
7
8
/**
* 重复测试
*/
@RepeatedTest(5)
@Test
void testRepeatedTest() {
log.info("重复测试中");
}

image-20230828095915653

全部的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.atguigu.boot;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.concurrent.TimeUnit;

/**
* @author Jason Gong
* @version 1.0
* @website https://qingling.icu
* @Date 2023/8/27
* @Description 测试junit5的相关注解
*/
@Slf4j
@DisplayName("Junit5Test测试类")
@SpringBootTest
public class Junit5Test {
/**
* @DisplayName 加在方法或者类上,为测试类或者测试方法设置展示名称
*/
@Disabled
@DisplayName("测试@DisplayName注解")
@Test
void testDisplayName(){
log.info("@DisplayName注解的测试");
}

/**
* 在执行的单元测试方法之前运行
*/
@BeforeEach
void testBeforeEach(){
log.info("执行了testBeforeEach()方法");
}

/**
* 在执行的单元测试方法之后运行
*/
@AfterEach
void testAfterEach() {
log.info("执行了testAfterEach()方法");
}

/**
* 在所有的单元测试执行之后执行
* 注意是静态方法
*/
@BeforeAll
static void testBeforeAll() {
log.info("执行了testBeforeAll()方法");
}

/**
* 在所有的单元测试执行之前执行
* 注意是静态方法
*/
@AfterAll
static void testAfterAll() {
log.info("执行了testAfterAll()方法");
}
/**
* 规定方法超时时间 超出时间测试出异常
*/
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}

/**
* 重复测试
*/
@RepeatedTest(5)
@Test
void testRepeatedTest() {
log.info("重复测试中");
}


}

3.断言

​ 断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告。

3.1 简单断言

用来对单个值进行简单的验证

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
@DisplayName("简单断言")
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());
}

3.2 数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等

1
2
3
4
5
@Test
@DisplayName("数组断言")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

3.3 组合断言

assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言

1
2
3
4
5
6
7
8
@Test
@DisplayName("组合断言")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}

3.4 异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。

1
2
3
4
5
6
7
8
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));

}

3.5 超时断言

Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间

1
2
3
4
5
6
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}

3.6 快速失败

通过 fail 方法直接使得测试失败

1
2
3
4
5
@Test
@DisplayName("快速失败")
public void shouldFail() {
fail("This should fail");
}

全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.atguigu.admin;

import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.*;



/*@RunWith*/

/**
* @BootstrapWith(SpringBootTestContextBootstrapper.class)
* @ExtendWith(SpringExtension.class)
*/
//@SpringBootTest
@DisplayName("junit5功能测试类")
public class Junit5Test {

/**
* 断言:前面断言失败,后面的代码都不会执行
*/
@DisplayName("简单断言")
@Test
void testSimpleAssertions() {
int cal = cal(3, 2);
//相等
assertEquals(6, cal, "业务逻辑计算失败");
Object obj1 = new Object();
Object obj2 = new Object();
assertSame(obj1, obj2, "两个对象不一样");

}

@Test
@DisplayName("数组断言")
void array() {
assertArrayEquals(new int[]{1, 2}, new int[]{1, 2}, "数组内容不相等");
}

@Test
@DisplayName("组合断言")
void all() {
/**
* 所有断言全部需要成功
*/
assertAll("test",
() -> assertTrue(true && true, "结果不为true"),
() -> assertEquals(1, 2, "结果不是1"));

System.out.println("=====");
}

@DisplayName("异常断言")
@Test
void testException() {

//断定业务逻辑一定出现异常
assertThrows(ArithmeticException.class, () -> {
int i = 10 / 2;
}, "业务逻辑居然正常运行?");
}

@DisplayName("快速失败")
@Test
void testFail(){
if(1 == 2){
fail("测试失败");
}

}

int cal(int i, int j) {
return i + j;
}
}

4.前置条件

​ JUnit 5 中的前置条件(assumptions[假设])类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 对象才会被执行;当条件不满足时,测试执行并不会终止

5.嵌套测试

​ JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}

@Nested
@DisplayName("when new")
class WhenNew {

@BeforeEach
void createNewStack() {
stack = new Stack<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}

@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}

@Nested//b
@DisplayName("after pushing an element")
class AfterPushing {

String anElement = "an element";

@BeforeEach
void pushAnElement() {
stack.push(anElement);
}

@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}

@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}

6.参数化测试

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利

利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型

@NullSource: 表示为参数化测试提供一个null的入参

@EnumSource: 表示为参数化测试提供一个枚举入参

@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参

@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试")
public void parameterizedTest(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() {
return Stream.of("apple", "banana");
}

7.迁移指南

在进行迁移(Junit4迁移到Junit5)的时候需要注意如下的变化:

  • 注解在 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。

五.指标监控

1.SpringBoot Actuator

​ 未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能

1.1 如何使用

1.引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.测试访问 http://localhost:8080/actuator/ endpoint节点

image-20230828110908056

image-20230828110928951

3.设置以web的方式访问所有的端点

1
2
3
4
5
6
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露所有的端点

设置之后就可以通过web(访问 http://localhost:8080/actuator/beans)的方式访问beans端点了

image-20230828113335033

支持的暴露方式

  • HTTP(web):默认只暴露health和info Endpoint,只能访问 http://localhost:8080/actuator/health和 http://localhost:8080/actuator/info
  • JMX(如Jconsole):默认暴露所有Endpoint,类似于cmd下打开jconsole,可以访问下面所有的endPoints
  • 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则

是通过web还是jmx方式访问的端点一览表

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

1.2 常用的EndPoints

http://localhost:8080/actuator/endPoint

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.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

最常用的Endpoint

  • Health:监控状况
  • Metrics:运行时指标
  • Loggers:日志记录

1.3 Health Endpoint

健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。

重要的几点:

  • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
  • 可以很容易的添加自定义的健康检查机制

设置显示health端点的详细信息

1
2
3
4
management:
endpoint:
health:
show-details: always

image-20230828142631189

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "MySQL",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 510769754112,
"free": 342642921472,
"threshold": 10485760,
"exists": true
}
},
"ping": {
"status": "UP"
}
}
}

1.4 Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;

  • 通过Metrics对接多种监控系统
  • 简化核心Metrics开发
  • 添加自定义Metrics或者扩展已有Metrics

查看完整的监控指标信息 http://localhost:8080/actuator/metrics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"names": [
"hikaricp.connections",
"hikaricp.connections.acquire",
"hikaricp.connections.active",
"hikaricp.connections.creation",
"hikaricp.connections.idle",
"hikaricp.connections.max",
"hikaricp.connections.min",
"hikaricp.connections.pending",
"hikaricp.connections.timeout",
"hikaricp.connections.usage",
"http.server.requests",
"jdbc.connections.max",
"jdbc.connections.min",
"jvm.buffer.count",
"jvm.buffer.memory.used",
"jvm.buffer.total.capacity",
"jvm.classes.loaded",
"jvm.classes.unloaded",
"jvm.gc.live.data.size",
"jvm.gc.max.data.size",
"jvm.gc.memory.allocated",
"jvm.gc.memory.promoted",
"jvm.gc.pause",
"jvm.memory.committed",
"jvm.memory.max",
"jvm.memory.used",
"jvm.threads.daemon",
"jvm.threads.live",
"jvm.threads.peak",
"jvm.threads.states",
"logback.events",
"process.cpu.usage",
"process.start.time",
"process.uptime",
"system.cpu.count",
"system.cpu.usage"
]
}

image-20230828143224633

查看某一个监控指标的详细信息(例如查看hikaricp.connections的详细信息) http://localhost:8080/actuator/metrics/hikaricp.connections

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "hikaricp.connections",
"description": "Total connections",
"baseUnit": null,
"measurements": [
{
"statistic": "VALUE",
"value": 10
}
],
"availableTags": [
{
"tag": "pool",
"values": [
"HikariPool-1"
]
}
]
}

1.5 管理Endpoint

开启与禁用Endpoints

  • 默认所有的Endpoint除过shutdown都是开启的。
  • 需要开启或者禁用某个Endpoint 配置模式为 management.endpoint.endpointName.enabled = true
1
2
3
4
management:
endpoint:
beans: #可以写health、metrics等端点
enabled: true
  • 或者禁用所有的Endpoint然后手动开启指定的Endpoint
1
2
3
4
5
6
7
8
management:
endpoints:
enabled-by-default: false #禁用所有的端点(jconsole中也无法使用)
endpoint:
beans:
enabled: true #逐一开启端点
health:
enabled: true

1.6 定制Endpoint

1.6.1 定制health endpoint

1.编写MyComHealthIndicator类继承AbstractHealthIndicator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.atguigu.boot.acutuator.health;

import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
* 自定义端点
* 类的后缀必须为HealthIndicator
*/
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {

/**
* 真实的检查方法 检查健康的方法
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 1){ //这里我们可以编写一个业务逻辑,例如数据库是否连接成功的业务逻辑
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
//builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}
builder.withDetail("code",100)
.withDetails(map);

}
}

myCom = MyComHealthIndicator - HealthIndicator 所以类的名字一定要以HealthIndicator为后缀

image-20230828150633322

1.6.2 定制 info endpoint

方式一: 编写配置文件

1
2
3
4
5
info:
appName: boot-admin #应用名
version: 2.0.1 #版本
mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
mavenProjectVersion: @project.version@

image-20230828151416487

方式二: 编写InfoContributor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.atguigu.boot.acutuator.info;

import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;

import java.util.Collections;


@Component
public class AppInfoInfoContributor implements InfoContributor {

@Override
public void contribute(Info.Builder builder) {

builder.withDetail("msg","你好")
.withDetail("hello","atguigu")
.withDetails(Collections.singletonMap("world","666600"));
}
}

如果我们没有删除上面配置文件的配置,这时我们访问 http://localhost:8080/actuator/info 的时候会将配置文件和InfoContributor中的信息共同返回

image-20230828152006549

1.6.3 定制Metrics信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyService{

Counter counter;

//构造器注入MeterRegistry
public MyService(MeterRegistry meterRegistry){
//访问 http://localhost:8080/actuator/metrics 的时候会多一个监控项myservice.method.running.counter
counter = meterRegistry.counter("myservice.method.running.counter");
}

public void hello() {
//调用hello方法的时候counter加1
counter.increment();
}
}

//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}

1.7 新增Endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.atguigu.boot.acutuator.endpoint;


import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.Map;


@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.....");
}
}

image-20230828153914619

image-20230828153902598

1.8 可视化

github地址: https://github.com/codecentric/spring-boot-admin

项目文档: https://docs.spring-boot-admin.com/current/getting-started.html

1.创建一个新的springBoot项目用作服务端(场景只需要选择web场景即可)

image-20230828154818630

2.引入依赖

1
2
3
4
5
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.3.1</version>
</dependency>

3.在启动类上添加注解

1
@EnableAdminServer

4.修改以下端口号以防和业务的端口冲突

1
server.port=8888

5.访问http://localhost:8888

image-20230828155537932

6.客户端上引入依赖

1
2
3
4
5
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.3.1</version>
</dependency>

7.客户端的配置文件添加以下的配置

1
2
spring.boot.admin.client.url=http://localhost:8888 #服务端的地址
management.endpoints.web.exposure.include=*

8.重启一下客户端,然后访问服务端的localhost:8080

image-20230828160606081

点击应用墙,点击需要查看信息的应用就可以看到应用的详细信息

image-20230828160731314

六.原理解析

1.Profile功能

为了方便多环境适配,springboot简化了profile功能

  • 默认配置文件 application.yaml;任何时候都会加载

  • 指定环境配置文件 application-{env}.yaml

  • 激活指定环境

    • 配置文件激活,在默认配置文件 application.yaml 中添加spring.profiles.active=xxxx
    • 命令行激活:java -jar xxx.jar –spring.profiles.active=prod
      • 修改配置文件的任意值,命令行优先
  • 默认配置与环境配置同时生效

  • 同名配置项,profile配置优先

@Profile注解

1
2
3
//@Profile("prod")
//可以标注在类和方法上
//标注在类和方法上表示在prod环境下启用

外部配置源

常用:Java属性文件、YAML文件、环境变量、命令行参数;

配置文件查找位置

下面的优先级覆盖上面的优先级

(1) classpath 根路径

(2) classpath 根路径下config目录

(3) jar包当前目录

(4) jar包当前目录的config目录

(5) /config子目录的直接子目录

配置文件加载顺序:

  1.  当前jar包内部的application.properties和application.yml
  2.  当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
  3.  引用的外部jar包的application.properties和application.yml
  4.  引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

总结: 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项