SpringBoot自学

本文最后更新于:3 年前

SpringBoot的大时代


1. 微服务

  • 微服务是一种架构风格
  • 一个应用拆分为一组小型服务
  • 每一个服务可以部署在自己的服务器上,运行在自己的进程内,也就是可以独立部署和升级,与单个应用无差
  • 服务之间的交互使用轻量级的HTTP交互
  • 服务围绕业务功能进行拆分
  • 服务可以由全自动部署机制独立部署
  • 去中心化(每一个服务可以用不同的语言来进行开发,也可以使用不同的存储技术)、服务自治

微服务的出现,将大应用拆分成多个小服务进行独立部署,会导致分布式的产生


2. 分布式

问题:

  • 远程调用
  • 服务发现
  • 负载均衡
  • 服务容错
  • 配置管理
  • 服务监控
  • 链路追踪
  • 日志管理
  • 任务调度

分布式的解决:SpringBoot + SpringCloud

3. 云原生

原生应用如何上云:Cloud Native

上云的困难:

  • 服务的自愈
  • 弹性伸缩(拥塞)
  • 服务隔离
  • 自动化部署
  • 灰度发布
  • 流量治理

SpringBoot官方文档架构


Spring官方网址

可以通过官方网址的Projects>SpringBoot进行SpringBoot的学习

其中OVERVIEW部分可以看到发布版本的更新情况以及更新的内容;LEARN则可以选择版本来进行对应的学习

本次学习所采用的是2.3.4版本的SpringBoot

SpringBoot2之HelloWorld

系统要求:

  • Java8及以上
  • Maven3.3+
  1. 配置Maven
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 <mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>*</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>

<profiles>
<profile>
<id>jdk-1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
</profile>
</profiles>

  1. HelloWorld

需求:浏览发送/hello请求,响应Hello,SpringBoot2

①创建一个普通的Maven项目,编写POM

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.ayy</groupId>
<artifactId>boot-01-helloworld</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- 导入SpringBoot父工程,版本是2.3.4 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>

<dependencies>
<!-- SpringBoot web开发启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

②编写主程序类,在main>java下创建com.ayy.boot.MainApplication Java类

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

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

/*
* @SpringBootApplication: 告知此为一个SpringBoot应用,带有此注解的也称之为主程序类;主配置类
* 在main方法里调用SpringApplication.run(主类.class,args);
*/
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}

③编写controller类,并运行main方法进行测试

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

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

//@ResponseBody //此注解用于表示此类的返回值均是写给浏览器的,而不是跳转某个页面等;
//@Controller
@RestController //上面二者合成了RestController
public class HelloController {
@RequestMapping("/hello") // 映射请求,表示希望浏览器发送hello请求
public String handle01(){
return "Hello,SpringBoot2!";
}
}
  1. 简化配置

在resources下创建一个application.properties的配置文件,所有的配置如端口号等,都可以写此处,当运行时,SpringBoot会读取里面的配置,若是无更改的则按照SpringBoot默认的配置行事。

当不知道什么配置可以写于其中时,可参照官方文档中的Application Properties的内容来进行设置。

  1. 简化部署

maven项目默认是打包为jar包

SpringBoot所打包是一个可执行的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>

通过maven自带的lifestyle的clean和package进行打包操作

对打好的包通过cmd命令行执行 java -jar 包名即可执行之!!

注意:

  • 需要取消cmd的快速编辑模式

最后展示一下此模块的目录结构:

目录结构


SpringBoot依赖管理特性

  • 父项目做依赖管理(自动版本仲裁机制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//pom.xml里面的父项目
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>

//starter-parent里面的父项目
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
</parent>

在spring-boot-dependencies这一项目里面,几乎声明了所有开发中常用的依赖的版本号,此即为自动版本仲裁机制
  • 开发导入starter场景启动器
1
2
3
4
5
6
7
8
9
10
11
1. spring-boot-starter-* : 此即代表某种场景
2. 只要引入starter,这个场景的所有常规需要的依赖我们都会自动导入
3. SpringBoot所有支持的场景:https://docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/using-spring-boot.html#using-boot-starter
4. 见到的 *-spring-boot-starter : 是第三方为我们提供的简化开发的场景启动器
5. 所有场景启动器最底层的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
  • 无需关注版本号,自动版本仲裁
1
引入依赖默认都可以不写版本号,除非引入的依赖是非版本仲裁的jar,则一定要写版本号
  • 可以修改版本号
1
2
3
4
5
6
7
8
1. 查看spring-boot-dependencies里面规定的当前依赖的版本所用的关键字
2. 在当前项目里面重写配置

注:利用的时MAVEN提供的特性:就近优先原则

<properties>
<mysql.version>5.1.43</mysql.version>
</properties>

SpringBoot自动配置特性

  • 自动配置tomcat(如在spring-boot-starter-web依赖下便带有)

    1. 引入tomcat依赖
    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <version>2.3.4.RELEASE</version>
    <scope>compile</scope>
    </dependency>
    1. 配置tomcat
  • 自动配置SpringMVC(如在spring-boot-starter-web依赖下便带有)

    1. 引入了SpringMVC全套组件
    2. 自动配好SpringMVC常用组件(功能)
  • 自动配好Web常见功能,如:字符编码问题

    SpringBoot帮我们配置好了所有web开发时的常见场景

  • 默认的包结构

    • 主程序所在的包及其下边的所有的子包都能被扫描(即无需配置包扫描,此为默认的包扫描规则)
    • 如果想要改变包扫描路径,可以通过在@SpringBootApplication(scanBasePackages=“com.ayy”)来改变
    • 或者通过一个@ComponentScan来指定扫描的路径,但此时因SpringBootApplication已经使用了其,故暂时不能将其与之写于一起来进行包扫描路径的替换
    1
    2
    3
    4
    5
    @SpringBootApplication
    等同于
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan("com.ayy")
  • 各种配置都拥有默认值

默认配置最终都是映射到某一个类上的

配置文件的值会绑定到某一个类上,这个类会在容器中创建对象

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

引入了哪些场景,这个场景的自动配置才会开启

SpringBoot的所有自动配置功能都在spring-boot-autoconfigure包里面


底层注解-@Configuration(组件添加)解析


在之前,我们对Spring进行一个组件的注册是通过在spring.xml配置文件里增添如下内容实现,在表示配置spring.xml之前,先进行两个类的构建

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
/*
* User类
*/
public class User {
private String name;
private String age;

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}

public User() {
}

public String getName() {
return name;
}

public User(String name, String age) {
this.name = name;
this.age = age;
}

public void setName(String name) {
this.name = name;
}

public String getAge() {
return age;
}

public void setAge(String age) {
this.age = age;
}
}


/*
* Pet类
*/
public class Pet {
private String name;

public String getName() {
return name;
}

@Override
public String toString() {
return "Pet{" +
"name='" + name + '\'' +
'}';
}

public Pet(String name) {
this.name = name;
}

public Pet() {
}

public void setName(String name) {
this.name = name;
}
}

我们通过spring.xml对其中的bean进行注册:

1
2
3
4
5
6
7
8
9
10
<beans>
<bean id="user01" class="User">
<property name="name" value="zhangsan"></property>
<property name="age" value="18"></property>
</bean>

<bean id="tomcat" class="Pet">
<property name="name" value="tom"></property>
</bean>
</beans>

以上便是Spring通过配置文件的方式,来实现组件的注册,经注册的组件可以在容器中找到

在SpringBoot中,可以通过@Configuration这个注解来替代spring.xml配置文件,即用一个含有@Configuration注解的类来进行组件的注册,而在需要注册的组件上,只需要通过@Bean注解来声明即可,如下类MyConfig所示:

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

import com.ayy.boot.bean.Pet;
import com.ayy.boot.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration // 告诉SpringBoot这是一个配置类(配置类就等于配置文件)
public class MyConfig {
@Bean // 给容器中添加组件,以方法名作为组件的id,返回类型就是组件类型。返回的值就是组件在容器中的实例
public User user01(){return new User("zhangsan","18");}

@Bean
public Pet tom(){return new Pet("tomcat");}
}
/*
* 被@Bean注解所注的方法的返回值即是注册在容器中的组件的实例对象,方法名即为实例对象的id,若是不满方法名作为对象id,可以在@Bean("myName")中进行组件id的重写
*/

需要特别注意的是:此时我们所注册在容器中的组件都是单例的,无论你通过容器如何getBean或多次getBean,甚至直接获取MyConfig组件(因其也是在配置文件下的类,故其也被注册在容器里)直接进行方法的调用,也依旧是单例!!

​ 这个单例的构成,与**@Configuration注解下的proxyBeanMethods的默认值为true有直接关系**,proxyBeanMethods即意为代理Bean方法,在其为true的情况下,我们通过getBean获得的MyConfig类的实例对象其实是代理对象,也即通过这个代理对象,我们无论怎么去调用对象里的方法,也只是从容器里面获取对应的组件而已;当代理Bean方法值为false时,才会是个普通的对象,通过调用其中方法,获得的实例则不相同。

​ 以下部分是体现代理对象调用方法后所得组件为容器中组件且为单例的实例:

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
/*
* @SpringBootApplication: 告知此为一个SpringBoot应用,带有此注解的也称之为主程序类
* 在main方法里调用SpringApplication.run(主类.class,args);
*/
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
// 返回IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
// 查看容器里的组件
String[] names = run.getBeanDefinitionNames(); // 获取容器中组件的实例对象的名字
for(String name:names)
System.out.println("NANE: "+name);
// 从容器中获取组件
MyConfig myConfig = run.getBean(MyConfig.class);
Pet jerry = run.getBean("jerry", Pet.class);
System.out.println(jerry==myConfig.tom());
System.out.println(myConfig);
}
}
/*
注:当我们打出myconfig的内容时,会发现其打印的内容为:
com.ayy.boot.config.MyConfig$$EnhancerBySpringCGLIB$$8a7037ca@7ea4d397
并非一个单纯的类,而在其中还夹杂了EnhancerBySpringCGLIB,即其被SpringCGLIB所提升,而很自然联想到是被提升为代理对象
小贴士:在Spring中一般这种事是jdk干的,但在SpringBoot中,这种事交由CGLIB干
而当我们修改MyConfig的注解为@Configuration(proxyBeanMethods=false)时,再运行程序,可以发现:
com.ayy.boot.config.MyConfig@62d0ac62
此时则为一个普通的容器中的组件
*/

proxyBeanMethods:代理bean的方法根据true/false,有以下两种模式:

  • Full(proxyBeanMethods=true) 全模式
  • Lite(proxyBeanMethods=false) 轻量级模式,因为不像全模式,在构建时需要查询容器中是否存在该实例,加快了运行速度,故为轻量级

在此处举一个单例的实例:比如我们的User里有一个成员变量是Pet,它们两个都是在容器中获得的组件,且此为User依赖Pet,那么通过单例,可以很好地体现这么一个依赖关系,因为User所占有的Pet,也是容器里面所独有的Pet,不存在这些个Pet相异的情况。

因此:当没有依赖组件时则用Lite轻量级模式;当需要依赖时,则需要使用Full全模式。

注:根据后面的学习来看,轻量级模式下的自动类配置,它的参数的获取,有大概率的可能是通过容器中获取。若是自己写的配置类(如笔者自己的MyCofig),使这个代理bean方法失效后,因笔者没有传入参数,故没有进行进一步的测试。因此笔者猜测:SpringBoot在处理配置类的参数时,直接获取的是容器中已有的组件,若是构建当前组件的实例,则对Full会进行单例查询,而对Lite则不查询,直接放入容器中。但随之而来的问题是:自动获取的传入参数如何保证是我们所想要的哪个呢?而框架本身的因为是与配置文件相绑定,所以只要是获取到的实参,都是所需的。

2021.9.7看,不懂上面的注说的啥

底层注解-@Import(导入组件)解析


除了上面所说的@Configuration加上@Bean可以给容器注册组件外,还有之前的@Component(表示为一个组件)、@Controller(表示为一个控制器)、@Service(表示为一个业务逻辑组件)、@Repository(代表它是一个数据库层组件)都能用。

@ComponentScan就是通过指定包扫描路径来实现组件导入,因为告知了Spring该去哪里扫描即哪里找可能是组件的类

@Import是给容器导入组件,可以写在配置类中或组件类中

它的参数是一个数组,这个数组里面写的是想要导入到容器中的组件的类型,它会调用该类的无参构造器来构造出该类的对象加入到容器中,所默认使用的id,即BeanName是全类名

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
//先在配置类MyConfig里写下以下内容,以在容器中导入对应的类实例对象

@Import({User.class, DBHelper.class})
@Configuration()
public class MyConfig {
@Bean
public User user01(){return new User("zhangsan","18");}

@Bean("jerry")
public Pet tom(){return new Pet("tomcat");}
}

// 之后我们通过MainApplication来打印导入的类,查看其打印情况
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
// 返回IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

// 查看Import注解导入的组件的名字
String[] beanNames = run.getBeanNamesForType(User.class);
System.out.println("<====================================>");
for(String bean:beanNames)
System.out.println("bean:" + bean);
DBHelper dbHelper = run.getBean(DBHelper.class);
System.out.println("bean:" + dbHelper);
}

}

// 打印结果如下:
<====================================>
bean:com.ayy.boot.bean.User
bean:user01
bean:ch.qos.logback.core.db.DBHelper@1d81e101
不难看出,我们通过导入进去的默认BeanName即我们在Spring.xml注册的id即为全类名,且其为无参构造得到的。

底层注解-@Conditional条件装配


条件装配:满足Conditional指定的条件,则进行组件注入!!!

Conditional相关注解.png

Conditional是个根注解,其下的许多注解可以按照名字的意思来进行对应的测试,下面的例子用@ConditionalOnBean来实现,其意为,当某个Bean存在时,则执行下面的内容:

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.通过对@Bean注解注释,使之不会注册到容器中,以下为此例的验证
@Import({User.class, DBHelper.class})
@Configuration()
public class MyConfig {
@Bean
public User user01(){return new User("zhangsan","18");}

// @Bean("jerry") 注解后,此为一个普通的方法,不具备成为组件的条件,则其不会在配置时被调用注册到容器中
public Pet tom(){return new Pet("tomcat");}
}


@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
// 返回IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

// 对注释了@Bean的组件进行测试,查看是否存在在容器中
boolean tom = run.containsBean("tom");
System.out.println("tom:" + tom);

boolean user01 = run.containsBean("user01");
System.out.println("user01:" + user01);
}
}

输出结果为:
tom:false
user01:true
// 通过输出结果即证明了没有加上注解@Bean的Javabean不会注册到容器中。则根据这一规则我们可以进行下面的测试


2. 当tom存在时,才注册user01到容器中,否则不注册
@Import({User.class, DBHelper.class})
@Configuration()
public class MyConfig {
@ConditionalOnBean(name={"tom"}) // 当容器中有个名为tom的组件时,才执行user01组件注册到容器中,否则不注册到容器中
@Bean
public User user01(){return new User("zhangsan","18");}

// @Bean("jerry") 注解后,此为一个普通的方法,不具备成为组件的条件,则其不会在配置时被调用,注入到容器中
public Pet tom(){return new Pet("tomcat");}
}

@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
// 返回IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

// 对注释了@Bean的组件进行测试,查看是否存在在容器中
boolean tom = run.containsBean("tom");
System.out.println("tom:" + tom);

boolean user01 = run.containsBean("user01");
System.out.println("user01:" + user01);
}
}

输出结果为:
tom:false
user01:false
// 通过结果可以得出,当名为tom的组件在容器中找不到时,user01也不会注册到容器中,此注解也可以放置到类上,以期满足某种条件,才执行类中的组件的注册。

底层注解-@ImportResource导入Spring配置文件


@ImportResource用于向SpringBoot中导入Spring的配置文件:spring.xml,即通过此注解可以将无法被SpringBoot所理解的组件注册到容器中。此注解写于配置类上即可

以下为一实例:

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
1.当在配置类MyConfig中没有加上此注解时:
<bean id="springXMLUser" class="com.ayy.boot.bean.User">
<property name="name" value="lisi"></property>
<property name="age" value="18"></property>
</bean>
<bean id="springXMLPet" class="com.ayy.boot.bean.Pet">
<property name="name" value="lisi's pet"></property>
</bean>
// 上述为spring.xml中的注册组件

@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
// 返回IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
// 对IOC容器进行查询,根据BeanName查询
boolean springXMLUser = run.containsBean("springXMLUser");
System.out.println("springXMLUser:" + springXMLUser);
boolean springXMLPet = run.containsBean("springXMLPet");
System.out.println("springXMLPet:" + springXMLPet);
}

}

// 输出结果为:
springXMLUser:false
springXMLPet:false

// 我们对配置类MyConfig进行@ImportResource("classpath:spring.xml")注解标识后,再运行,可得如下输出结果
springXMLUser:true
springXMLPet:true
// 此时即实现了对spring中的组件进行了注册

底层注解-@ConfigurationProperties配置绑定

在以前,我们对一些常规配置的内容是写在my.properties中然后通过绑定的机制来将之内容写入Javabean中,这个过程较为繁琐,而在SpringBoot中,我们可以通过将配置信息写在application.properties,然后通过注解@ConfigurationProperties来实现绑定,且绑定的形式有两种!下图是之前绑定的方法的一个流程显示:

ConfigurationProperties.png

法①

通过在Javabean类Car中进行注解:@Component 和 @ConfigurationProperties(prefix = “mycar”) 来实现配置文件中的内容与该Javabean的绑定,并注册为容器中的一个组件。代码如下所示:

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
/*
* 只有在容器中的组件,才能使用SpringBoot提供的强大功能,因此需要通过@Component注解来将这个组件加入到容器中,
* 然后对prefix为mycar的配置内容进行获取
*/
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
private String brand;
private String price;

public String getBrand() {
return brand;
}

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

public String getPrice() {
return price;
}

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

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

public Car(String brand, String price) {
this.brand = brand;
this.price = price;
}

public Car() {
}
}

// 以下内容是application.properties的配置内容:
server.port=8888

mycar.brand=TESLA
mycar.price=280000

// 接着我们在HelloController类中通过映射请求来实现这个绑定的体现:
@RestController //上面二者合成了RestController
public class HelloController {
@Autowired
private Car car;

@RequestMapping("/car")
public Car myCar(){
return car;
}
}

// 紧接着我们访问 localhost:8888/car 即可获得输出于浏览器上的结果
{"brand":"TESLA","price":"280000"}

法②

删去Car类中的@Component组件并通过在MyConfig这个配置类中增加注解@EnableConfigurationProperties(Car.class)来开启Car类的属性配置功能

其中@EnableConfigurationProperties的作用有如下两点:

  1. 开启Car组件的配置绑定功能
  2. 把Car这个组件自动地注册到容器中

此类用法多用于我们使用第三方的jar包中的类的时候,我们不能轻易地去修改别人的源代码,因此可以通过这样的方式来实现组件的配置绑定及加载到容器中。


自动配置-自动包规则原理


自动包配置原理,是在SpringBoot应用下才生效的,即在SpringBoot应用下,可以自动加载配置类,即会自动往容器中导入组件,那么这个实现得从注解@SpringBootApplication先看起;

1
2
3
4
5
6
@SpringBootApplication
<=====>等价于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
接下来我们对它们逐个解析
  1. @SpringBootConfiguration

往此注解内部点去,可以发现其内部的核心是@Configuration,即注明此类是配置类,也就是说,我们的MainApplication类也是配置类

  1. @ComponentScan

此即为自动包扫描的配置,配置其下的目录及其子包都会被扫描

  1. @EnableAutoConfiguration

我们通过Ctrl+左键点击进去后发现,@EnableAutoConfiguration注解由如下注解组成:

  • @AutoConfigurationPackage

  • @Import({AutoConfigurationImportSelector.class})

我们先着重说一下,@AutoConfiguraionPackage这个注解!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 我们点进去@AutoConfigurationPackage这个注解有以下内容
@Import({Registrar.class})
public @interface AutoConfigurationPackage {}
// 我们发现,@AutoConfiguraionPackge里的注解是导入了Registrar组件,那么导入组件则是一个一个导入,我们先继续点进去该类看下
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}

public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
}
}
//上面是这个类的方法,我们看第一个方法registerBeanDefinitions(),传入的第一个参数metadata是一个注解元数据类型,所获取到的其实质是这个注解所注的类的元数据信息,而这个注解是合成注解,因此所获取到的是MainApplication这个标注了@SpringBootApplication注解的类,再看该方法里面的内容,它通过metadata获取到了我们主类的包名,然后还给它弄成了一个数组,对这个包名下的组件进行了注册!!

由此可见,我们自动包规则原理,便是基于这个Registrar这个类,利用这个类,给容器导入一系列的组件。将指定的标注了这个注解(@AutoConfiguraionPackage)或利用之合成的注解(@EnableAutoConfiguraion或@SpringBootApplication)的类所在的包进行了组件注册!!

自动配置-初始加载自动配置类


上面我们解释了@EnableAutoConfiguraion中的@AutoConfiguraionPackage,紧接着我们讲一下另一个注解@Import({AutoConfigurationImportSelector.class}),我们来详细谈一下这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.在进入AutoConfigurationImportSelector类后,我们看到一个方法:
public String[] selectImports(AnnotationMetadata annotationMetadata){...},其中该方法有一行代码是: getAutoConfigurationEntry(annotationMetadata); // 此方法用于给容器批量导入一批组件
2.上面所说的那个给容器批量导入一批组件的方法,其内调用了:
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); // 通过调用此方法获取到所有需要导入到容器中的配置类
3.上述的那个方法是怎么知道导入这些需要导入的配置类的呢?往里面点,我们发现:
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
// 它利用工厂加载来加载内容,那么所加载的内容是什么呢?
4.再往下点击,可以发现其所加载的内容是:
Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader){...}; // 即通过一个Map来获取到所有的组件
5.那么这些组件是从哪里得到并加载的呢?通过以下对loadSpringFactories的debug过程便可略知一二:
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); // 不难看出,它是从一个META-INF/spring.factories的位置来加载文件。Spring会默认扫描我们当前系统里面所有的META-INF/spring.factories位置的文件!!
6.我们可以通过External Libraries来找对应的存在META-INF/spring.factories位置的jar包,查看其中内容。而最核心的包便是spring-boot-autoconfigure这个包。通过对这个包的内容进行查看,看到其下META-INF/spring.factories中有一行注为Auto Configure的内容,其后紧跟着127个自动配置类!!也就是说,文件里面写死了spring-boot一启动就要加载到容器中的所有配置类。
7.我们可以通过getBeanDefinitionCount()来查看的确是有这127个组件的存在,那么此时又存在另一个问题:它那么大,不应该会导致系统很卡嘛?
虽然我们127个场景的所有自动配置启动的时候默认全部加载,但最终会按需配置!!!
这个按需配置就是利用了之前所学的条件装配规则!!!(@ConditionnalOnClass(使用者所需要导入的类.class)

AutoConfigurationImportSelector.png

上图不难看出,默认导入的组件有127个之多。

自动配置中一些有趣的东西

1
2
3
4
5
6
7
8
9
10
	    @Bean
@ConditionalOnBean({MultipartResolver.class})
@ConditionalOnMissingBean(
name = {"multipartResolver"}
)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
return resolver;
}
// 上面这就是MultipartResolver文件上传解析器,当容器中有该类并且找不到该类名为multipartResolver的组件时,则通过下面的函数,利用参数由容器中获取,然后将之返回,并将其名字改为函数名即multipartResolver,然后重新放回到容器中!
// 此例用于将不规范的文件上传解析器名字进行更改,以规范化!

SpringBoot默认会在底层配好所有的组件,但是如果用户自己配置了的话,则就以用户的优先
下面以字符配置(HttpEncodingAutoConfiguration)为例,并先对默认配置进行解释,再展示自我配置的方式:

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
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ServerProperties.class})
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(prefix="server.servlet.encoding",value = {"enabled"},matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
private final Encoding properties;

public HttpEncodingAutoConfiguration(ServerProperties properties) {
this.properties = properties.getServlet().getEncoding();
}

@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
return filter;
}
/*...
...
...
*/
}

​ 从上面的源码中我们可以得知HttpEncodingAutoConfiguration是一个配置类,且其开启了类ServerProperties的配置绑定,并将之加入于容器中,然后我们的是Web项目,且是类型为Servlet的,又因为自动导入了SpringMVC,因此对应的CharacterEncodingFilter类也存在,最后的注解标识是实不实现都可以,因此满足自动配置此类的条件,则可以继续往下执行!
​ 它的构造函数会从刚刚加入到容器中的ServerProperties组件获取,然后把一些内容交予本类的成员properties,之后的characterEncodingFilter()这个函数,则利用该成员进行字符编码的设置!!由此可见,若想通过DIY方式配置SpringBoot的环境,可以通过修改配置文件(即application.properties)来实现;

​ 当然也可以通过接下来的手段实现,因为在进行组件的注册时,其有条件装配规定的约束,当容器中无该类才执行,即若是使用者自行注册,则不会再次于其中注册,此即满足了用户优先原则,且也为我们DIY配置环境提供了一个方法,就是自己定义配置类进行注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 以下以字符编码CharacterEncodingFilter类为例,先展示无修改任何配置的情况下的正常输出:
@RestController
public class HelloController {
@RequestMapping("/hello") // 映射请求,表示希望浏览器发送hello请求
public String handle01(@RequestParam("name") String name){
return "Hello,SpringBoot2!" + name;
}
}
// 当我们在浏览器中键入:'localhost:8888/hello?name=张三' 则在浏览器中会显示'Hello,SpringBoot2!张三',由此可见SpringBoot默认在底层的确给我们配置好了所有组件!

// 接下来,通过修改配置文件application.properties来进行乱码操作!
我们通过查找HttpEncodingAutoConfiguration类发现其prefix为server.servlet.encoding,然后在application.properties中进行修改,将之修改为server.servlet.encoding.charset=ISO-8859-1
则通过浏览器进行输入测试,会发现,已乱码为:Hello,SpringBoot2!??
由此可见,配置文件DIY便利着实有效!

// 接下来,通过自定义配置类,然后注册组件实现字符编码DIY
@Bean
public CharacterEncodingFilter characterEncodingFilter() {return null;}
//上述代码实则在运行的时候会报错,因为该组件在底层其他部分也有重要的应用,但可以通过这种方法注册组件,而不是使用系统默认的组件配置。

总结:

  • SpringBoot先加载所有的自动配置类(xxxAutoConfiguration)

  • 每个自动配置类按照条件装配进行生效,默认都会“绑定”配置文件指定的值,这个绑定是通过利用参数xxxProperties(这是个类,用于配置属性绑定的)获得的。xxxProperties和它对应的配置文件进行了配置绑定!

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

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

  • 只要用户有自己配置的,就以用户的优先

  • 定制化配置(DIY配置)

    用户通过自定义配置类再通过@Bean注解来使用自己想配置的组件

    用户通过看底层源码对应的xxxProperties中的prefix来获取前缀,通过application.properties来进行修改(结合tab提示)

xxxAutoConfiguration(自动配置类) —> 导入了一大堆组件 —> 通过xxxProperties去获取值 —> 通过application.properties去重设置值


SpringBoot编写逻辑

  1. 引入对应的场景依赖
  2. 查看自动配置了哪些组件
    • 自行分析,引入场景对应的自动配置一般都生效了
    • 配置文件中debug=true开启自动配置报告 Negative(不生效) / Positive(生效)
  3. 是否需要修改
    • 参照文档修改配置项
    • 自行分析,xxxProperties绑定的配置文件的前缀,然后找到注入的部分
    • 自定义加入或者替换组件
      • @Bean@Component
    • 自定义器xxxCustomizer(目前IDon’tKnow)

开发小技巧-Lombok

Lombok这个东西可用于简化JavaBean的开发

安装依赖及插件的过程如下:

1
2
3
4
5
1.依赖安装
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

2.插件安装,通过Settings>Plugins下搜索lombok进行安装即可!

lombok的几个重要注解:

  • @Getter/@Setter:作用于类上,生成所有成员变量的get/set方法;作用于成员变量上,则只对该变量生成get/set方法。可以设置访问权限(@Getter(value=“AccessLevel.PUBLIC”))和是否懒加载。
  • @ToString:会自动给Javabean的成员变量们构造toString函数,可以通过of/exclude来指定/排除某些成员变量生成于toString方法中。
  • @EqualsAndHashCode:生成equals和hashcode方法。
  • @NoArgsConstructor:自动给Javabean创造无参构造器
  • @AllArgsConstructor:自动给Javabean创造全参构造器
  • @RequiredArgsConstructor:生成包含final和@NonNull注解的成员变量构造器
  • @Data:@Getter+@Setter+@ToString+@EqualsAndHashCode+@RequiredArgsConstructor
  • @Builder:作用于类上,快速地为类实现建造者模式。可以链式赋值(初始化的时候),若是需要修改要么通过set,要么在实体类的@Builder(toBuilder=true),但它会返回一个全新的对象。
1
User user = User.builder().id(1).username("ayy").build();

注:若需部分成员构造器,则可以自行编写或利用IDEA的自动编写功能


开发小技巧-dev-tools

此物需要增加依赖于pom.xml中,用于制造伪热更新(实质通过restart形式实现,而热更新是通过reload形式实现),以下是其依赖:

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

当我们修改我们的代码文件后,不需要通过关闭再打开项目来实现刷新,而直接通过Ctrl + F9重新编译项目即可,然后就实现了刷新!

如果想要使用真正的热更新,可以付费购买插件JRebel。


开发小技巧-Spring Initializer

创建SpringBoot项目通过File>New>Project>Spring Initializer以GUI界面创建SpringBoot,其中要啥starter就自行勾选啥,然后创建的时候要联网,会自动帮你导jar包(就是自动添加依赖啦)。

不过一开始关于mvn和.gitignore不会用到,删去即可。

而在src下,我们可以看到src>main>java + resources。在resources目录下,可以看到application.properties + static(包) + templates(包)

static包用于存储静态资源,如css 、js;templates包用于存放页面


配置文件-yaml用法


基本语法

  • key: value #k,v之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • ‘#’表示注释
  • ‘’ 与 “” 表示字符串内容,会比如 转义/不转义

数据类型

  • 字面量:单个的、不可再分的值 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

在build>plugins下的spring-boot-maven-plugin插件内添加如下配置,以在打包时舍弃之,减少包的大小
<configuraion>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuraion-processor</artifactId>
</exclude>
</excludes>
</configuraion>

Web-静态资源规则与定制化

1. 静态资源目录

只要静态资源放在类路径下:

/static(or/publicor/resourcesor/META-INF/resources)

则可以直接通过:当前项目根路径/ + 静态资源名 进行访问

原理:静态映射/**,即拦截所有的请求,而controller也是如此。

在运行的时候,请求进来,先去找controller看能不能处理,不能处理的所有请求则交给静态资源处理器,若是静态资源处理器也无法处理则报告404

改变默认的静态资源路径:

1
2
3
spring:
resources:
static-locations: [classpath:/xxx/]

2. 静态资源访问前缀

默认无前缀。

之所以要使用这个东西是因为,如果是一个web项目,需要登录后才可以执行某一些操作,若是拦截器拦截/**,则静态资源也会被拦截。为了拦截器可以放行静态资源,因此可以通过静态资源加上访问前缀来过滤掉它们。

可以通过如下方式在配置文件中设置:

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

3. 支持webjar静态资源访问

webjar即是将如css、js等文件通过jar包的形式给出,可通过依赖获得

获取webjar

1
2
3
4
5
6
<!-- 以下是一个jQuery的webjar的依赖 -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>

访问形式:localhost:8080/webjars/…(资源详细路径)


Web-welcome与favicon功能

1. 欢迎页支持

用于直接ip:port访问项目,会显示index.html欢迎页

  • 静态资源路径下放置index.html
    • 可以配置静态资源路径
    • 不可以配置静态资源访问前缀,否则会导致index.html不能默认访问

2. 自定义Favicon

用于更改web项目的小图标,这个在静态资源路径下放置favicon.ico即可,需注意浏览器缓存可能导致的无法显示。同样的也不可以配置静态资源的访问前缀,否则会导致其失效


静态资源配置原理

  • SpringBoot启动默认加载 xxxAutoConfiguration类(自动配置类)

  • SpringMVC功能的自动配置类 WebMvcAutoConfiguration生效

    • 那么它给容器中配了些什么呢?

    比如:OrderedHiddenHttpMethodFilter(用来兼容rest风格,表单可以提交PUT、DELETE等)、OrderedFormContentFilter(表单内容过滤器),然后有一个叫做WebMvcAutoConfigurationAdapter这么一个配置类(那么它肯定也在容器中)。接下来研究一下它:

    1
    2
    3
    4
    5
    @Configuration(proxyBeanMethods = false)
    @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
    @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
    • 先看到配置文件,它让WebMvcProperties和ResourceProperties跟对应的配置文件绑定。spring.mvc==WebMvcProperties 、spring.resources==ResourceProperties

​ 1. 配置类只有一个有参构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 有参构造器所有参数的值都会从容器中确定
/*
* resourceProperties 获取和spring.resources绑定的所有值的对象
* mvcProperties 获取和spring.mvc绑定的所有值的对象
* beanFactory Spring的bean工厂
* HttpMessageConverters 找到所有的HttpMessageConverters
* ResourceHandlerRegistrationCustomizer 找到资源处理器的自定义器
* DispatcherServletPath
* ServletRegistrationBean 注册servlet、filter...
*/
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
  1. 资源处理的默认规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// isAddMapping这个玩意是看你要不要使用静态资源的默认规则,可以通过spring.resources.add-mappings来选择使用或关闭,默认使用
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
// 这个是用来设置静态资源的缓存的存活时间,可以通过spring.resources.cache.period设置
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
// 注册/webjars/**静态资源,且说明其资源路径是META-INF/resources/webjars下,那么之后访问webjars资源则通过访问/webjars/所需访问的资源的层级目录即可!所以这里是webjars的规则(/webjars/**的所有请求,通过类路径下的/META-INF/resources/webjars下拿)
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
// 这一段是对静态资源默认路径的配置,先通过mvcProperties获取/**下的所有请求,通过getStaticLocations获取的区域下面拿取静态资源。静态资源都有缓存策略
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}

}
}
  1. 欢迎页的处理规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// HandlerMapping:处理器映射,保存了每一个handler能处理哪些的请求 
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}

// 以下是欢迎页处理器映射的构造函数,不难看出,它写死了欢迎页只能在/**下
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
// 欢迎页存在且/**等于静态资源路径才可以使用之,否则不行
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage.get());
this.setRootViewName("forward:index.html");
} else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
this.setRootViewName("index");
}

}

请求参数处理

请求映射(这里说的不是RequestMapping,而是Rest风格的请求映射注解)

  • @xxxMapping
  • Rest风格支持*(使用HTTP请求方式动词来表示对资源的操作)*
    • 以前是通过:/getUser获取用户 /deleteUser删除用户 /editUser修改用户 /saveUser保存用户
    • 现在是通过: /user (就只这一个访问路径)
      • GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
    • 核心Filter:HiddenHttpMethodFilter
      • 用法: 表单method=post , 隐藏域type=hidden,_method=PUT
      • 需注意还需要手动开启:spring.mvc.hiddenmethod.filter.enable=true;
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
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String PostUser(){
return "Post User";
}

@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String DeleteUser(){
return "Delete User";
}

@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String PutUser(){
return "Put User";
}

@RequestMapping(value = "/user",method = RequestMethod.GET)
public String GetUser(){
return "Get User";
}


源码部分:
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.hiddenmethod.filter",
name = {"enabled"},
matchIfMissing = false
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}

Rest原理(表单提交,且需要使用REST时)

  • 表单提交会带上_method=PUT
  • 请求过来的时候会被HiddenHttpMethodFilter拦截
    • 请求是否是POST,且是否正常
      • 获取到_method的值
      • 原生request(post),包装模式requestWrapper重写了getMethod方法,返回的是传入的值(_method=XXX)
      • 兼容以下请求:PUT DELETE PATCH等
      • 过滤链放行的时候使用的是wrapper。以后调用的getMethod方法是调用requestWrapper的。

Rest使用客户端工具:

  • 如postman直接发生put、delete等方式请求,无需filter重新包装

上述的@RequestMapping(value=“/user”,method=“RequestMethod.POST”)这些注解,可以更改为以下的注解(由上述注解合成而来):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  	@PostMapping("/user")
public String PostUser(){
return "Post User";
}
@DeleteMapping("/user")
public String DeleteUser(){
return "Delete User";
}
@PutMapping("/user")
public String PutUser(){
return "Put User";
}
@GetMapping("/user")
public String GetUser(){
return "Get User";
}

**如何更改默认的_method为我们想要的名字呢?**请看以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration(proxyBeanMethods = false)    // 组件无依赖故Lite型的
public class MyConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_m");
return hiddenHttpMethodFilter;
}
}

// 通过使用“用户优先原则”来进行HiddenHttpMethodFilter组件的注册,并设定MethodParam的值,重而实现更改默认的_method!


请求映射原理


普通参数与基本注解

  • 注解:

@PathVariable、@RequestHeader、@RequestParam、@CookieValue、@RequestAttribute、@RequestBody、@MatrixVariable

  • Servlet API

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

  • 复杂参数

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

  • 自定义对象参数

可以自动类型转换和格式化,可以级联封装

1. 注解:

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
// @PathVariable一般接收的是Rest风格的请求,即表单请求,因此使用GetMapping,然后里面的value/path用希望使用者访问的路径和传入的数据,而数据通过{数据名}的形式括起来,在函数的参数部分通过@PathVariable(数据名)来获取数据,并且给到参数;当然也可以通过Map<String,String>的方式来进行直接获取全部的参数,此时在@PathVariable里不需写任何参数。
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String userName,
@PathVariable Map<String,String> pv){
Map<String,Object> map = new HashMap<>();
map.put("id",id);
map.put("username",userName);
map.put("pv",pv);
return map;
}


// @RequestHeader获取的是浏览器页面的请求头的kv值,可以通过@RequestHeader(数据名)来获取数据;也可以通过@RequestHeader Map<String,String> rh来获取所有的请求头的内容,此时再@RequestHeader内不需写任何内容。
public Map<String,Object> getCar(@RequestHeader("User-Agent") String userAgent,@RequestHeader Map<String,String> rh){
Map<String,Object> map = new HashMap<>();
//=======
map.put("userAgent",userAgent);
map.put("rh",rh);
return map;
}


// @RequestParam获取的请求参数,可以通过@RequestParam(数据名)来获取数据的值;也可以通过@RequestParam Map<String,String>来获取所有的请求参数的信息,这时不需要写数据名,默认全部访问
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String userName,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> rh,
@RequestParam("age") Integer age,
@RequestParam("interest") List<String> interests,
@RequestParam Map<String,String> rp){

Map<String,Object> map = new HashMap<>();
//=======
map.put("age",age);
map.put("interests",interests);
map.put("rp",rp);
return map;
}

// @CookieValue注解用于获取当前访问内容的cookie值。可通过以下来种方式进行访问:①@CookieValue(cookie的名称) 基本数据类型,则这样获取到的是这个cookie对应的value值;②@CookieValue(cookie的名称) Cookie cookie,则这样获取到的是cookie的对象,里面有其name和value和供我们获得
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String userName,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> rh,
@RequestParam("age") Integer age,
@RequestParam("interest") List<String> interests,
@RequestParam Map<String,String> rp,
@CookieValue("_ga") String _ga,
@CookieValue("_ga")Cookie cookie){

Map<String,Object> map = new HashMap<>();
//=======
map.put("_ga",_ga);
System.out.println(cookie.getName() + "-->" + cookie.getValue());
return map;
}

// @RequestBody获取请求体,这个只有post请求才有请求体。可以通过这样来获取表单的kv数据。@RequestBody String content
@PostMapping("/save")
public Map getRequestBody(@RequestBody String content){
Map map = new HashMap<>();
map.put("内容",content);
return map;
}
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
<!-- 搭配上述内容使用的HTML文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>com.ayy.test</title>
</head>
<body>
<h1>HelloIndexHtml</h1>
<form action="/user" method="get">
<input value="GET-TYPE" type="submit"/>
</form>
<form action="/user" method="post">
<input value="POST-TYPE" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_m" value="DELETE" type="hidden"/>
<input value="DELETE-TYPE" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_m" value="PUT" type="hidden"/>
<input value="PUT-TYPE" type="submit"/>
</form>
<ul>
<a href="car/3/owner/zhangsan?age=18&interest=basketball&interest=tennis">car/{id}/owner/{username}</a>
<li>@PathVariable 路径变量</li>
<li>@RequestHeader 获取请求头</li>
<li>@RequestParam 获取请求参数</li>
<li>@CookieValue 获取Cookie值</li>
<li>@RequestAttribute 获取request域属性</li>
<li>@RequestBody 获取请求体</li>
<li>@MatrixVariable 矩阵变量</li>
</ul>

<form action="/save" method="post">
<br/>
<input type="password" placeholder="Pwd" name="passwd" /><br/>
<input type="text" placeholder="Usr" name="usrName"><br/>
<input type="submit" value="提交"><br/>
</form>

<a href="/cars/sell;price=300000;brand=BYD,AUTO,TESLA">MatrixVariable111 /cars/sell;price=300000;brand=BYD,AUTO,TESLA</a>
<a href="/cars/boss;id=666/emp;id=888">MatrixVariable222 /cars/boss;id=666/emp;id=888</a>
</body>
</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
// @RequestAttribute获取request域的属性。request可以通过setAttribute方法来设置属性,而request是在一次请求中生效。这就要说下forward和redirect两个的区别先。
// forward是直接转发,它只需要客户端发起一次的请求即可;而redirect(重定向/间接转发)是客户端发起两次请求。服务器端在响应第一次请求的时候,让浏览器再向另一个URL发出请求,从而达到转发的目的。
// 因此本次测试,需要采用forward直接转发的方式
@Controller
public class RequestController {
/**
* 此例子用于显示HttpServletRequest 和 @RequestAttribute注解提取request的域属性,需用forward进行直接请求转发,redirect这个是间接转发要进行
* 两次请求,不符要求
* 在默认情况下,无@ReponseBody默认进行页面跳转
*/

@GetMapping("/goto")
public String gotoNextPage(HttpServletRequest request){
request.setAttribute("user","张三");
request.setAttribute("status",200);
return "forward:/success";
}

@ResponseBody
@GetMapping("/success")
public Map<String,Object> successPage(@RequestAttribute("user") String user,
@RequestAttribute("status") Integer status,
HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
map.put("r_attribute:",user);
map.put("r_servletRequset",request.getAttribute("user"));
return map;
}
}
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
// MatrixVariable是矩阵变量,需要与PathVariable路径变量结合起来使用。且一般存放的是cookie的kv
// 我们通过PathVariable设置访问路径,而MatrixVariable是在路径变量;后的那一大串,当然也可以通过另一个路径变量相隔开。
// 矩阵变量的例子: localhost://car/sell;price=200000;brand=BYD,AUTO,TESLA
// 其中同一个k的多个v也可以用分号拆开(brand=BYD;brand=AUTO;brand=TESLA),这里的sell就是PathVariable,后面的;到结尾都是MatrixVariable。
// 那么通过路径来显示就是/car/{path;price=xxx;brand=xxx,yyy,zzz} ,这里可以看出MatrixVariable必须依靠PathVariable而存在
// 需要跟RequestParam进行区分/car/{path}?price=xxx&brand=xxx,这里可以看出PathVariable和RequestParam是独立的两个个体
@GetMapping("/cars/{path}")
public Map getCarSell(@PathVariable("path") String path,
@MatrixVariable("price") Integer price,
@MatrixVariable("brand") List<String> brand){
Map<String,Object> map = new HashMap<>();
map.put("path",path);
map.put("price",price);
map.put("brand",brand);
return map;
}

// 需注意,以下此物默认关闭,需自定义组件UrlPathHelper来开启
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper=new UrlPathHelper();
// 开启不移除分号功能,这样才可以使MatrixVariable生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}

// 若是存在/car/1;age=20/2;age=30这种情况,则可以利用MatrixVariable的参数pathVar来设定区域,否则若是直接根据k取age会都取到第一个。
// /cars/boss;id=666/emp;id=888
@GetMapping("/cars/{path1}/{path2}")
public Map getCarDetail(@MatrixVariable(value = "id",pathVar = "path1") Integer bid,
@MatrixVariable(value = "id",pathVar = "path2") Integer eid){
Map<String, Object> map = new HashMap<>();
map.put("bid",bid);
map.put("eid",eid);
return map;
}

// 小拓展:一般来说我们若是将cookie功能禁掉,是无法访问到session的,因为session->jsessionId相关联->而jsessionId是存放在cookie->cookie在每一次的请求中都会被带上,但若是禁掉,就无法获得jessionId,就无法获得session。但现在可以通过矩阵变量的方式去获得(具体不清晰!!!)

2. Servlet API

就如上面所说的那么多的Servlet API,是怎么通过Resolver(即参数解析器)来实现对应参数获取的?此外,注解获得的参数,也是通过参数解析器实现参数的获取。

以下以HttpServletRequest这个ServletAPI来展示如何通过参数解析器来获取之:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//ServletRequestMethodArgumentResolver
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;
// WebRequest == Request + Response + ...
private <T> T resolveNativeRequest(NativeWebRequest webRequest, Class<T> requiredType) {
T nativeRequest = webRequest.getNativeRequest(requiredType);
if (nativeRequest == null) {
throw new IllegalStateException("Current request is not of type [" + requiredType.getName() + "]: " + webRequest);
} else {
return nativeRequest;
}
}
// 此时即返回原生的request。因此这是通过resolver实现ServletAPI参数获取的一个展示。可通过debug逐步寻找

3. 复杂参数

对于复杂参数:Model、Map它俩存放的区域是request的请求域(渲染时存放的),即request attribute那个东西。以下例子通过直接转发来体现之:访问localhost:8080/params -> localhost:8080/success

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
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest req,
HttpServletResponse resp){
map.put("map1","map content");
model.addAttribute("md","model content");
req.setAttribute("req","request content");

Cookie cookie = new Cookie("c1","v1");
resp.addCookie(cookie);
return "forward:/success";
}

@ResponseBody
@GetMapping("/success")
public Map<String,Object> successPage(@RequestAttribute(value ="user",required = false) String user,
@RequestAttribute(value = "status",required = false) Integer status,
HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
map.put("r_attribute:",user);
map.put("r_servletRequset",request.getAttribute("user"));
map.put("map1",request.getAttribute("map1"));
map.put("md",request.getAttribute("md"));
map.put("req",request.getAttribute("req"));
return map;
}

4. 自定义对象的参数

它是通过一个叫“数据绑定”的东西:当页面提交的请求数据(GET、POST)都可以和对象属性进行绑定,包括级联绑定。以下是数据绑定的一个例子

1
2
3
4
5
@PostMapping("/saveUser")
public Person saveUser(Person person){
return person;
}

1
2
3
4
5
6
7
8
<form action="/saveUser" method="post">
姓名:<input name="userName" value="zhangsan"/><br/>
年龄:<input name="age" value="18"/><br/>
生日:<input name="birth" value="2000/1/5"/><br/>
宠物姓名:<input name="pet.name" value="cat"/><br/>
宠物年龄:<input name="pet.age" value="5"/><br/>
<input type="submit">
</form>

响应处理


响应JSON

jackson.jar + ResponseBody

1
2
3
4
5
6
7
8
9
10
11
    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
web场景自动引入了Json场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.7.RELEASE</version>
<scope>compile</scope>
</dependency>

可以通过jackson.jar包和@ResponseBody注解返回给前端json数据:

1
2
3
4
5
6
7
8
@Autowired
Person person;

@ResponseBody
@GetMapping("/response/test")
public Person person() {
return this.person;
}

那么这个返回值是如何变成了json数据的格式呢?

先前说过ArgumentResolver参数解析器,在确定方法的参数值的时候,会用各种参数解析器来确定;

而现在有ReturnValueHandler,故可知springmvc对返回值的所有解析也是采取了返回值解析器的方法。


ReturnValueHandler原理


HTTPMessageConverter原理


内容协商原理


基于请求参数的内容协商原理


自定义MessageConverter


浏览器与PostMan内容协商完全适配


视图解析与模板引擎


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!