文章目录
SpringBoot 中文网:https://springdoc.cn/spring-boot
1. 介绍
1.1 环境要求
| 环境&工具 | 版本(or later) |
|---|---|
| SpringBoot | 3.0.5+ |
| IDEA | 2021.2.1+ |
| Java | 17+ |
| Maven | 3.5+ |
| Tomcat | 10.0+ |
| Servlet | 5.0+ |
| GraalVM Community | 22.3+ |
| Native Build Tools | 0.9.19+ |
1.2 快速入门
场景:发起
/hello请求, 返回hello springboot3!
-
创建工程

此处没有使用Spring Initializr, 一般创建会用这个, 更快更方便。 -
将项目设置为
springboot项目, 引入web场景启动器的依赖<!-- 设置当前项目的父工程为 spring-boot-starter-parent --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.2</version> </parent> <dependencies> <!-- 导入 web 场景启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> -
创建主程序
@SpringBootApplication public class Main { public static void main(String[] args) { // 自动创建 ioc 容器, 加载配置, 启动 tomcat 服务器软件 SpringApplication.run(Main.class, args); } } -
创建
HelloController类@RestController @RequestMapping("/hello") public class HelloController { @RequestMapping public String hello(){ return "hello springboot3!"; } } -
在
resources目录下创建application.propertiesserver.port=80 # 修改服务器端口号, 此处可先不用 -
启动主程序, 访问
http://localhost:80/hello即可。主打一个速度
没有任何配置文件, 全部采用默认的约定
目录结构:

1.3 原理解析
1.3.1 为什么依赖不需要写版本
-
每个 boot 项目都有一个父项目
spring-boot-starter-parent -
parent的父项目是spring-boot-dependencies, 作为 版本仲裁中心, 把所有常见的 jar 的依赖版本都声明好了 (不常见的就自己声明版本)
如何修改默认版本?
比如我们在 spring-boot-dependencies 中查看到了 mysql 的版本为 8.x
<mysql.version>8.3.0</mysql.version>
而我们想使用 5.x 版本的
我们只需要在我们的 pom 文件中加入如下配置即可
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
1.3.2 starter是什么
SpringBoot 提供了一种叫做 starter 的概念, 它是一组预定义的依赖项的集合, 意在简化 Spring 应用程序的配置和构建过程。starter 包含了一组相关的依赖项, 以便在启动应用程序时自动引入需要的库、配置和功能。
主要作用如下:
- 简化依赖管理:Spring-Boot-Starter 通过捆绑和管理一组相关的依赖性, 减少了手动解析和配置依赖项的工作。只需引入一个相关的 Starter 依赖, 即可获取应用程序的全部依赖。
- 自动配置:Spring-Boot-Starter 在应用程序启动时自动配置所需的组件和功能。通过根据类路径和其他设置的自动检测, Starter 可以自动配置 Spring Bean、数据源、消息传递等常见组件, 从而使应用程序的配置变得简单和维护成本降低。
- 提供约定优于配置:Spring-Boot-Starter 遵循约定优于配置的原则, 通过提供一组默认设置和约定, 减少了手动配置的需要。它定义了标准的配置文件命名约定、默认属性值、日志配置等, 使得开发者可以更专注于业务逻辑而不是繁琐的配置细节。
- 快速启动和开发应用程序:Spring-Boot-Starter 使得从零开始构建一个完整的 Spring Boot 应用程序变得容易。它提供了主要领域(如web开发, 数据访问, 安全性, 消息传递等)的 Starter, 帮助开发者快速搭建一个具备特定功能的应用程序原型。
- 模块化和可扩展性:Spring-Boot-Starter 的组织结构使得应用程序的不同模块可以进行分离和解耦。每个模块可以有自己的 Starter 和依赖项, 使得应用程序的不同部分可以按需进行开发和扩展。
启动器就等于 Spring 中的 (依赖+配置文件), 引入一个场景下的启动器, 就等于帮我们引入了需要的依赖和默认的配置文件。
例如:
spring-boot-starter-web 帮我们导入了所有的 web 所需要的依赖(点进去看一下), 包括 json, tomcat, web, webmvc 等。如 json 处理帮我们导入了需要的 databind jsr310 等等。

并且帮我们自动配置好了 Web 常见功能,如:字符编码问题 characterEncodingFilter, 文件上传与下载 multipartResolver 等等
@SpringBootApplication
public class Main {
public static void main(String[] args) {
// 获取IOC容器
ConfigurableApplicationContext run = SpringApplication.run(Main.class, args);
// 查看里面的所有组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
}

官方提供的所有启动器:https://springdoc.cn/spring-boot/using.html#using.build-systems.starters
命名规范:
- 官方提供的场景:命名为
spring-boot-starter-* - 第三方提供的场景:命名为
*-spring-boot-starter
1.3.3 @SpringBootApplication 注解的功效
@SpringBootApplication 添加到启动类上, 我们看一下这个接口的源码
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}
解析:
-
@SpringBootConfiguration指代这是一个 SpringBoot 配置类。它是什么呢?看源码@Configuration @Indexed public @interface SpringBootConfiguration {}@Configuration代表它是一个配置类,@Indexed不知道什么玩意, 应该是指定优先级的吧。由上可知@SpringBootConfiguration可以知道它的作用是指代这是一个配置类, 所以我们可以在 Main 类中加入我们需要的组件。@SpringBootApplication public class Main { public static void main(String[] args) { // 自动创建 ioc 容器, 启动 tomcat 服务器软件 SpringApplication.run(Main.class, args); } // 加入组件 @Bean public Object object(){ return new Object(); } } -
@EnableAutoConfiguration自动加载 SpringBoot 默认的配置类
该注解源码如下@AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration {}分析1:
@AutoConfigurationPackage
再看它的源码@Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage {}导入了一个类, 我们点进去再看这个类的源码
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0])); } @Override public Set<Object> determineImports(AnnotationMetadata metadata) { return Collections.singleton(new PackageImports(metadata)); } }打个断点进行观察, 它的作用是先查查这个注解在哪个类上, 然后找到这个类所在的包, 在把该包下所有需要注册的 Bean 都进行注册。

分析2:
@Import(AutoConfigurationImportSelector.class)
观察它的作用, 查看该类源码public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!this.isEnabled(annotationMetadata)) { return NO_IMPORTS; } else { AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata); // 比较重要, 看它的源码 return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } }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); }-
利用
getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件 -
调用
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);获取到所有需要导入到容器中的候选配置类,共有152个

-
利用工厂加载得到所有组件
-
这 152 个是从
"META-INF/spring.factories"位置来加载一个文件。默认扫描我们当前系统里面所有"META-INF/spring.factories"位置的文件

-
经过过滤, 最终我们会进行按需开启配置, 所有最后只会剩下我们需要的一部分

按需加载实际上就是按条件进行装配
比如AopAutoConfiguration类,在初始时是加载进来的,上面的第二个加载组件,之后进行判断如果配置文件中有 spring.aop.auto 且值为true(matchIfMissing = true指代如果配置文件中没有则默认为true)则配置该类, 而AspectJAutoProxyingConfiguration由于缺少Advice.class并没有装配进来。 而再看ClassProxyingConfiguration, 缺少org.aspectj.weaver.Adivce并且spring.aop.proxy-target-class设置为 true 则导入, 可知符合条件是导入成功的。

我们可以通过以下方法进行验证ConfigurableApplicationContext run = SpringApplication.run(Main.class, args); String[] beanDefinitionNames = run.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { System.out.println(beanDefinitionName); // 查看有没有该类, 经检验, 确实如此 }
-
-
@ComponentScan默认扫描当前类所在的包以及子包下的注解 (所以 com.lh.controller.HelloController 被加入了容器)
所以我们在 Main 类上添加
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
与一个 @SpringBootApplication 是一个效果的
如何改变默认的包扫描规则?
默认的包结构扫描规则
-
主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
-
想要改变扫描路径,
@SpringBootApplication(scanBasePackages="com.lh")@SpringBootApplication(scanBasePackages="com.lh") public class MainApplication { }注意
@SpringBootApplication中含有@ComponentScan, 所以两个注解不能同时使用 -
或者使用三个注解配置的方式, 在
@ComponentScan指定扫描路径@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan("com.lh")
1.3.4 配置文件
在application.properties中的配置项
-
各种配置拥有默认值
-
默认配置最终都是映射到某个类上,如:
MultipartProperties配置文件的值最终会绑定每个类上,这个类会在容器中创建对象 (IOC容器中你可以看到)
-
按需加载所有自动配置项
-
非常多的starter
引入了哪些场景这个场景的自动配置才会开启
SpringBoot所有的自动配置功能都在
spring-boot-autoconfigure包里面

1.4 知识点讲解
1.4.1 @Conditional
条件装配:满足Conditional指定的条件,则进行组件注入 当有多个配置类时该注解使用的较多,如果容器中存在某个组件或不存在某个组件才进行添加新的组件,防止组件的冲突!!!
如我对某个实体类通过 @Component 加入了容器, 在配置类中又不小心使用 @Bean 加入容器, 且 id 一致, 那就会报错。又或者说组件之间有依赖关系, 比如如果人没有了, 那么它的宠物也不需要加入容器了, 所以这个很重要

@Configuration(proxyBeanMethods = true) // 见 Spring 讲解
//@ConditionalOnBean(name="pet") 放在最上面则意味着有 pet 组件该配置类中的组件才会被注册
public class MyConfig {
@Bean
@ConditionalOnBean(name="pet")
public User user(){
User user = new User("tiger", 18);
// user 组件依赖了 pet 组件
user.setPet(pet());
return user;
}
public Pet pet(){ //容器中没有该组件
return new Pet("ergou");
}
}
如上, 容器中没有二狗, 那虎虎也没了
1.4.2 原生配置文件引入
XML 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dog" class="com.lh.pojo.Pet">
<property name="name" value="ergou"></property>
</bean>
</beans>
在配置类中使用注解引入
@ImportResource("classpath:beans.xml") //指定路径
主程序中测试
System.out.println(run.containsBean("dog")); //true
1.5 修改默认配置
如下:给容器中加入了文件上传解析器。SpringBoot 默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先
@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
// public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
@Bean 放在方法上后, 即放入容器中, 那么它的参数 MultipartResolver 也会从容器中找并进行自动注入
该类注入条件
- 容器中有
MultipartResolver类型的组件 - 容器中没有名字为
multipartResolver组件
怎么知道有没有 MultipartResolver 类型的组件呢?我们来到 MultipartAutoConfiguration 类中

发现如果容器中没有, 它会帮我们创建一个并加入, 且名字为 multipartResolver。
那第一个代码有什么用啊?
确实, 如果用户没有自定义上传解析器, 确实没什么用, 因为容器中存在名为 multipartResolver 的组件, 所以不会执行。
但是如果用户自定义了, 那么第二个代码不会执行, 即不会帮我们创建一个文件上传解析器。然后第一段代码开始执行, 如果用户定义的该组件名字不是 multipartResolver, 那么就会将用户这个组件获取, 并加入容器(方法名即组件名 multipartResolver)
我们可以验证以下, 在容器中加入自定义的 MultipartResolver
@Bean
public MultipartResolver mr() {
return new MultipartResolver() {
@Override
public boolean isMultipart(HttpServletRequest request) {
return false;
}
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return null;
}
@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
}
};
}
运行测试程序
ConfigurableApplicationContext run = SpringApplication.run(Main.class, args);
String[] names = run.getBeanNamesForType(MultipartResolver.class);
System.out.println(Arrays.toString(names)); // [mr, multipartResolver]
MultipartResolver mr = run.getBean("mr", MultipartResolver.class);
MultipartResolver multipartResolver = run.getBean("multipartResolver", MultipartResolver.class);
System.out.println(mr == multipartResolver); // true
结果:确实有两个, 并且它俩是同一个, 只是有两个名字而已。
总结:
-
SpringBoot先加载所有的自动配置类
xxxAutoConfiguration -
每个自动配置类按照条件进行生效,默认都会绑定配置文件 (xxxProperties.class) 指定的值。xxxProperties 和application.properties 配置文件进行了绑定
-
生效的配置类就会给容器中装配很多组件
-
只要容器中有这些组件,相当于这些功能就有了
-
只要用户有自己配置的,就以用户的优先
-
定制化配置
用户直接自己@Bean替换底层的组件
用户去看这个组件是获取的配置文件什么值就去修改
步骤:xxxxxAutoConfiguration —> xxxxProperties.class里面拿值 ----> application.properties 修改


如何查看修改配置的名称??
- 去官网
- 看底层源码
找到想要配置的AutoConfiguration,比如characterEncoding的
关键点

去 spring-boot-autoconfigure 下找想要修改的配置的包下的 xxxAutoConfiguration 类
@EnableConfigurationProperties({xxx.class}) // 进入xxxx类中看属性
// xxx类
@ConfigurationProperties(
prefix = "server",// 找前缀
ignoreUnknownFields = true
)
public class ServerProperties{
private Integer port; // 端口号 修改 server.port = 8888
}
characterEncoding配置比较麻烦…如下





server.servlet.encoding.charset=GBK
这里你可能比较迷惑, 只看到有注解写了 prefix="server", 其他都没写啊。
其实是这么来的, ServerProperties 中规定了前缀为 server(第二张图), 它里面有一个属性 servlet, 所以访问它是通过 server.servlet, 但是 servlet 是一个类, 它也会有自己的属性, 该类里面有一个属性叫 encoding, 上面有一个注解 @NestedConfigurationProperty, 意思是我作为一个属性同时也是类也要绑定属性文件(出现多层嵌套, 类中类), 所以通过属性名进行绑定配置文件, 而 Encoding 也是一个类, 最终要访问编码就是 server.servlet.encoding.charset。
那你可能又疑惑, 我们要指定前缀啊, 它怎么知道层级之间的名字的
观察第二章图, servlet 是一个类, 也作为 ServerProperties 类的一个字段, 这个字段名就表示了层级之间的关系, 与配置文件进行绑定时的命名, 因为当只有单个类的时候
@Component
@ConfigurationProperties(prefix="user")
public User{
private String name;
}
我们正是通过 user.name 来进行绑定的。属性名来作为最后一级进行访问, (这种绑定也是通过 getter, setter 来实现的)
同理 server.servlet 是由于 servlet 是一个属性, 所以不需要指定前缀, 同理 encoding 也是, 层层嵌套。
2. 配置文件
2.1 统一配置管理概述
SpringBoot 工程下, 进行统一的配置管理, 你想设置的任何参数(端口号、项目根路径、数据库连接信息等)都集中到一个固定位置和命名的配置文件 (application.properties 或 application.yaml) 中
配置文件应该放置在 Spring Boot 工程的 src/main/resources 目录下。这是因为 src/main/resources 目录是 Spring Boot 默认的类路径(classpath), 配置文件会被自动加载并供应用程序访问。
我们看 spring-boot-starter-parent 下的源码有一段如下
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
</resource>
- 位置:
resources目录下, 必须命名 application*.properties/.yaml/.yml 。 - 如果同时存在 application.properties | application.yml(.yaml), properties 的优先级更高
- 配置基本都有默认值
功能配置参数:https://springdoc.cn/spring-boot/application-properties.html#appendix.application-properties
举例:在 resources 目录下创建 application.properties 文件, 配置端口号和项目根路径
server.port=80
server.servlet.context-path=/ergou
再进行访问路径就需要是 http://localhost:80/ergou/hello
2.1.1 自定义属性配置
当我们自定义了一个属性, 我们该如何进行读入呢?两种方式
使用原来的方式
@Properties(path) + @Value + 自定义 properties
-
熟悉的
jdbc.propertiesdog.name=二狗 -
在
@Controller层注入@RestController @RequestMapping("/hello") @PropertySource("classpath:jdbc.properties") public class HelloController { @Value("${dog.name}") private String name; @RequestMapping public String hello(){ System.out.println("name = " + name); return "hello springboot3!"; } }结果是成功注入并使用的
@Value + application.properties
server.port=80
server.servlet.context-path=/ergou
dog.name=二狗
controller 层注入
@RestController
@RequestMapping("/hello") // 注意没有使用 @PropertyResource
public class HelloController {
@Value("${dog.name}")
private String name;
@RequestMapping
public String hello(){
System.out.println("name = " + name);
return "hello springboot3!";
}
}
也可以成功注入并使用!!!
❓ 为什么
因为 application.properties 是默认加入容器的, 所以 @Value 可以直接从容器中取到, 就不需要 @PropertyResource 指定位置, 而 jdbc.properties 默认是不加入容器的, 如果不指定位置, @Value 就取不到 dog.name, 就会报错 Error creating bean with name 'helloController': Injection of autowired dependencies failed。

从文件标识也可以看出, 同为 properties 类型文件, application.properties 已经被识别为 SpringBoot 的配置文件了, 而 jdbc.properties 没有被识别到。
2.2 yaml 配置介绍和使用
基于层次结构的数据序列号格式, 相对于 properties 更易读
2.2.1 基本语法
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格(idea中可以使用 tab)
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- '#'表示注释
- 字符串无需加引号,如果要加,
''表示字符串内容不会被转义,""表示字符串内容会被转义'张三 \n 李四'\n作为换行符"张三 \n 李四"\n作为普通字符串输出
# 可以使用的高级内容
person:
hello: 111
${random.uuid}
${random.int}
2.2.2 数据类型
-
字面量:单个的、不可再分的值。data、boolean、string、number、null
k: v -
对象:键值对的集合。map、hash、set、object
#行内写法: k: {k1:v1,k2:v2,k3:v3} #或 k: k1: v1 k2: v2 k3: v3 -
数组:一组按次序排列的值。array、list、queue
#行内写法: k: [v1,v2,v3] #或 k: - v1 - v2 - v3
2.2.3 测试举例
application.yaml 文件
person:
user-name: 二狗 # 字符串类型的可以不加引号
# '二狗 \n 二狗'单引号会将\n作为字符串输出 双引号会将\n作为换行输出
boss: false
birth: 2002/10/26
age: 20
interest:
- 犯贱
- 傻逼
animal: [阿猫,阿狗]
#score: {english: 80,math: 90}
score:
english: 80
math: 90
salarys:
- 9999.98
- 9999.99
pet:
name: 阿狗
weight: 99.99
all-pets: #等价于allPets
sick:
- {name: 阿狗, weight: 99.99}
health:
- {name: 阿花, weight: 66.66}
两个实体类
// person 类
@Component
@ToString
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
// Pet类
@Data
@ToString
public class Pet {
private String name;
private Double weight;
}
yaml 文件中的 user-name 与实体类的 userName 会进行自动映射
🐯 三种方式进行注入
方式一:@PropertySource + @Value
@Component
@Data
@ToString
// @PropertySource("classpath:aaa.properties")
public class User {
@Value("${person.id}")
private String id;
@Value("${person.money}")
private Integer money;
@Value("${person.user-name}")
private String userName;
@Value("${person.boss}")
private Boolean boss;
@Value("${person.birth}")
private Date birth;
@Value("${person.age}")
private Integer age;
@Value("${person.pet}")
private Pet pet;
@Value("${person.interests}")
private String[] interests;
@Value("${person.animal}")
private List<String> animal;
@Value("${person.score}")
private Map<String, Object> score;
@Value("${person.salaries}")
private Set<Double> salaries;
@Value("${person.all-pets}")
private Map<String, List<Pet>> allPets;
}
缺点:实际上这个是无法完成注入的, 因为 @Value 只能读取单个值, 对于实体类对象、集合、数组等是无法读取的, 所以一般只能读取简单类型, 并且由于要把名字写全(${person.*}), 所以比较麻烦。
这里只是用了 @Value 这一个, 而没有使用 @PropertySource 注解, @PropertySource 指定是哪个配置文件里面的,如果不写,默认为(application.properties/yml)
方式二:@PropertySource + @ConfigurationProperties
@ConfigurationProperties 配置属性,一般需要指定前缀,属性名与配置类里的相同, 需要保证名字一致, 因为是使用的 getter setter 方法进行的注入。
@ConfigurationProperties(prefix = "person")
// @PropertySource(value = "classpath:aaa.properties")
@Component // 放入容器
@Data
@ToString
public class User {
private String id;
private Integer money;
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> salaries;
private Map<String, List<Pet>> allPets;
}
同样这里也是指定没有使用 @PropertySource
测试类
// controller类测试
@RestController
public class HelloController {
@Autowired
Person person;
@RequestMapping("/person")
public Person person(){
return person;
}
}
输出结果
{
"id": "e0d91ef3-8f7c-429e-8bd2-9a37e74b4469",
"money": 657522604,
"userName": "二狗",
"boss": false,
"birth": "2002-10-25T16:00:00.000+00:00",
"age": 20,
"pet": {
"name": "阿狗",
"weight": 99.99
},
"interests": [
"犯贱",
"傻逼"
],
"animal": [
"阿猫",
"阿狗"
],
"score": {
"english": 80,
"math": 90
},
"salaries": [
999.98,
999.99
],
"allPets": {
"sick": [
{
"name": "阿狗",
"weight": 99.99
}
],
"health": [
{
"name": "阿花",
"weight": 66.66
}
]
}
}
注意:都必须要加上 @Component, 因为这些注入都是由容器帮我们做到的。
方式三:@EnableConfigurationProperties + @ConfigurationProperties
如果引入的是第三方库且第三方库没有设置 @Component,可以使用该方法进行设置。这里可能不常用, 就使用另一个实体类简化描述, 这是第三方库中的实体类。注意: @EnableConfigurationProperties 是加在配置类上的注解, 它相当于一种声明, 来声明哪些类的属性由配置文件注入。而 @ConfigurationProperties 放在类上用来指明, 如果被 @EnableConfigurationProperties 声明由配置文件注入, @ConfigurationProperties 用来指定前缀等一系列信息。
SpringBoot 有关于文件上传的类 MultipartProperties
@ConfigurationProperties(prefix = "spring.servlet.multipart", ignoreUnknownFields = false)
public class MultipartProperties {}
这个注解的意思是配置文件 properties, yaml 中的 spring.servlet.multipart 作为前缀的属性都与我这个类中的字段进行绑定。但是注意到这个地方并没有使用 @Component, 并没有加入到容器中, 所以这里只做了规定, 并没有实现注入。
💡 关于某个场景的 yaml 配置的那些选项都会对应一个类用来加入到容器中, 通常以 properties 结尾, 比如这个 MultipartProperties。
但是还有一个配置类
@AutoConfiguration
@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 {}
第五个注解 @EnableConfigurationProperties(MultipartProperties.class)
它有两个作用:
- 开启
MultipartProperties.class的属性和配置文件绑定功能 - 注册到容器中
如此就可以使用了。
但是需要注意此处并没有使用 PropertySource, 意味着我们的配置必须都放在 application.yaml 等文件夹下才能识别到。
💡 当我们想知道某个类的功能配置时, 我们可以来到这个 **AutoConfiguration 类中, 看 @EnableConfigurationProperties 中的类, 进去后就可以看到需要的配置文件对应绑定的类。
2.2.4 可用依赖
在使用 yaml 时,如果是自定义的属性是没有提示的,可加入依赖提高开发效率
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<-- 文件打包时该configuration-processor不参与打包 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<proc>none</proc>
</configuration>
</plugin>
</plugins>
</build>
2.2.5 优先级
在进行读取文件时,application.properties的优先级大于application.yaml,所以如果某一个属性在两个文件中都配置了,那么application.properties设置的会生效
2.2.6 问题解决
-
避免使用
user.usernameuser.nameuser.home等属性配置,与系统配置发生冲突(计算机用户名称、home),因为Spring会优先系统配置。@ConfigurationProperties(prefix = "person") @PropertySource(value = "classpath:userproperties.yml") @ToString @Data public class Person { // 这两种情况下都会与系统中的 name 发生冲突,而不会使用 person.name,所以要避免使用,可以改为 usr-name // @Value("${user-name}"} // @Value("${name}"} @Value("${usr-name}") private String userName; } -
classpath:usr.yml与classpath: usr.yml不同,一定不要加空格,否则找不到文件 -
读取
yml或properties配置文件中文乱码@PropertySource(value = "classpath:usr.yml", encoding = "UTF-8") // 加入 encoding = "UTF-8"
2.3 多环境配置和激活
2.3.1 需求
在 SpringBoot 中, 可以使用多环境配置来根据不同的运行环境(如开发、测试、生产)加载不同的配置。SpringBoot 支持多环境配置让应用程序在不同的环境中使用不同的配置参数, 例如数据库连接信息、日志级别、缓存配置等。
例如, 可以创建 application-dev.yml、application-prod.yml 和 application-test.yml等文件。在这些文件中, 可以使用 YAML 语法定义各自环境的配置参数。最后通过 spring.profiles.active 属性指定当前的环境, SpringBoot 会加载相应的 YAML 文件。
也可以通过命令行参数来指定当前的环境。例如, 可以使用 --spring.profiles.active=dev 来指定使用开发环境的配置。
通过上述方法, SpringBoot 会根据当前指定的环境来加载相应的配置文件或参数, 从而实现多环境配置。这样可以简化在不同环境直接的配置切换, 并且确保应用程序在不同环境中具有正确的配置。
所以我们的配置不一定都要放在 application.yaml 这一个中, 内容比较多。利用这个可以放在多个配置文件中, 如 application-druid.yaml 配置 druid 连接池信息。
2.3.2 测试举例
举例, 三个配置文件
application.yaml
dog:
username: 二狗
password: 123
spring:
profiles:
active: dev,test # 激活外部配置文件, 只给后缀名就行
application-dev.yaml
dog:
age: 18
application-test.yaml
dog:
id: ${random.uuid}
username: 傻狗
测试的内容
- 能否将外部文件也读取到
- 最终
User对象是否同时有 id, age, username, password 四个属性 application.yaml与application-test.yaml的dog.username冲突, 谁会生效?(外部的application-test.yaml生效)
输出: 1,2 均可以, 最终 username 生效的是外部文件
{
"id": "a7dbc719-61dd-4aa8-8904-12342f3ff742",
"username": "傻狗",
"age": 18,
"password": "123"
}
3. 整合 SpringMVC
引入 spring-boot-starter-web 场景即可
3.1 web 相关配置
application.yaml
# springmvc 相关的 web 配置
server:
port: 80 # 端口号
servlet:
context-path: /boot
所有的配置可以去官网查, 下面介绍几个重要的配置参数:
server.port: 指定应用程序的HTTP服务器端口号, 默认使用8080作为端口号server.servlet.context-path: 设置应用程序的上下文路径, 即应用程序在URL中的基本路径, 默认情况下, 上下文路径为空, 即/spring.mvc.view.prefix和spring.mvc.view.suffix: 这两个属性用于配置视图解析器的前缀和后缀。spring.resources.static-locations:配置静态资源的位置, 静态资源可以是CSS、JS、图片等。默认情况下, 静态资源放在classpath:/static等目录下。spring.http.encoding.charset和spring.http.encoding.enabled:这两个属性用于配置HTTP请求和响应的字符编码。spring.http.encoding.charset定义字符编码的名称(例如 UTF-8),spring.http.encoding.enabled用于启用或禁用字符编码的自动配置。
3.2 静态资源访问
3.2.1 静态资源目录
只要静态资源放在类路径下:called /static(or /public or /resources or /META-INF/resources)
源码:点进去就能看

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
使用默认配置进行访问静态资源
访问方式:当前项目根路径/ + 静态资源名。 注意,不需要给静态目录,不需要加 META/resource/ 目录名

请求进来,先去找 Controller 看能不能处理。不能处理的所有请求又交给静态资源处理器。静态资源也找不到就 404
改变默认的文件夹位置
当我们将图片放在 pictures 目录下时, 是访问不到该头像的, 因为静态资源路径只有那四个文件夹才可以。这个时候需要我们手动修改静态资源访问路径
spring:
web:
resources:
static-locations: [classpath:/picture/] # 为数组形式, 多个文件夹之间用逗号分割
# 改变默认访问的静态资源文件夹,那么原来的默认的四个文件夹就都不可以用了。

修改后只有红色标注的文件夹下的静态资源可以访问, 原来默认的四个都不可以了。

修改后可访问 pictures 下的静态资源
spring.web.resources.static-locations 为访问的静态资源文件夹,即你放静态资源的文件夹
3.2.2 静态资源访问前缀
默认无前缀, 为 /**, 即所有请求都可以
spring:
mvc:
static-path-pattern: /res/**
spring.mvc.static-path-pattern 为在浏览器中访问的前缀
当前项目根路径 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
如:localhost:8080/res/头像.jpg
注意:默认的项目根路径是 /, 默认的静态资源访问前缀为 /**。
server:
servlet:
context-path: /boot # 项目根路径修改
spring:
mvc:
static-path-pattern: /res/** # 静态资源访问前缀修改
3.2.3 webjar
应该是前后端不分离的时候用, 不进行细学了。
自动映射 /webjars/**
https://www.webjars.org/
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version> <-- 目前最新3.6.0 -->
</dependency>

jquery的js文件打包成了jar包供我们使用,我们使用时可以引用

访问路径:http://localhost:8080/webjars/jquery/3.6.3/dist/jquery.js

3.2.4 欢迎页支持
欢迎页是什么?比如我们的项目在启动时, http://localhost:8080, 首先出现的是一个 404。当我们设置了欢迎页, 那么出现的将是这个, 而不是 404。
使用欢迎页需要注意的地方
- 欢迎页命名必须为
index.html,index.jsp都不行 - 欢迎页需要放到静态资源路径下
- 不要配置静态资源访问前缀, 否则
index.html将不能被访问到
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
resources:
static-locations: [classpath:/pictures/]
假如我们设置了 spring.mvc.static-path-pattern: /res/** 并设置资源访问路径为 [classpath:/pictures/]

此时访问 localhost:8080 和 localhost:8080/res 都访问不到, 任何方式都访问不到了, 只能通过 controller 层处理
-
导入 springboot 的 thymeleaf 模板引擎
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> -
将页面
index.html放在templates目录下(原因如下)// 默认配置 public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html"; //xxx.html -
配置 controller
@Controller @RequestMapping public class HelloController { @GetMapping public String index(){ return "index"; // 只写页面名称即可 } @GetMapping("/index") public String index1(){ return "index"; } } -
在页面引入约束
<html lang="en" xmlns:th="http://www.thymeleaf.org">
此时 localhost:8080 与 localhost:8080/index 都可以访问到欢迎页
默认欢迎页的寻找规则:它首先在配置的静态内容位置寻找一个 index.html 文件。 如果没有找到,它就会寻找 index 模板。 如果找到了其中之一,它就会自动作为应用程序的欢迎页面使用。
原因可看下方的欢迎页处理规则, 待做
3.2.5 自定义 favicon
As with other static resources, Spring Boot checks for a
favicon.icoin the configured static content locations. If such a file is present, it is automatically used as the favicon of the application.
favicon.ico 放在静态资源目录下即可。
# spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
这里有几个注意点:
- Google Chrome 浏览器有时候请求不到 favicon.ico
- Edge 浏览器有时候会保留之前的 favicon.ico, 如果我们更换 favicon.ico, 可能一直还是原来的
解决方法:
Ctrl + F5强制刷新浏览器- 勾选禁用缓存

3.3 请求参数处理
3.3.1 请求映射
rest 使用与原理
rest 使用原理
以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
用法: 表单提交指定 method=XXX
下面来测试一下
// controller 层, 四种请求分别返回不同的内容
@RestController
@RequestMapping("/user")
public class HelloController {
@GetMapping
public String getMethod(){
return "REST-GET";
}
@PostMapping
public String postMethod(){
return "REST-POST";
}
@PutMapping
public String putMethod(){
return "REST-PUT";
}
@DeleteMapping
public String deleteMethod(){
return "REST-DELETE";
}
}
在 template 下创建 index.html 页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello SpringBoot</title>
</head>
<body>
<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="put">
<input value="REST-PUT" type="submit"/>
</form>
<form action="/user" method="delete">
<input value="REST-DELETE" type="submit"/>
</form>
</body>
</html>
运行,发起请求

点击四个按钮返回结果分别是
REST-GET
REST-POST
REST-GET
REST-GET
怎么回事, PUT 和 DELETE 返回的都是 GET 请求???(未开启RestFul风格前只有 GET, POST 请求生效, 其他请求都会被改为 GET 请求方式)原因未知
我们看底层 WebMvcAutoConfiguration 类的一个组件
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
// boolean matchIfMissing() default false;
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
它的作用是当容器中没有 HiddenHttpMethodFilter 时为容器放入一个 HiddenHttpMethodFilter 组件
作用是指定是否开启 RESTFul 风格(由Filter实现), 这里默认值是 false, 根据这个配置文件前缀, 我们去开启它
spring:
mvc:
hiddenmethod:
filter:
enabled: true
再次访问
结果还是如此, 并没有变化, 为什么开启了还没用, 我们需要看底层究竟是如何实现的
点击查看 HiddenHttpMethodFilter 源码
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 获取原生的 request 请求
HttpServletRequest requestToUse = request;
// 前提 method 必须为 POST 才能进行处理, 并且没有出现错误
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// 获取表单参数名为 _method 的值, 如 put
String paramValue = request.getParameter(this.methodParam);
// 不为 null 且不为空
if (StringUtils.hasLength(paramValue)) {
// 转为大写 PUT
String method = paramValue.toUpperCase(Locale.ENGLISH);
// 判断是否合法属于 PUT, DELETE 或 PATCH
// private static final List<String> ALLOWED_METHODS = List.of(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name());
if (ALLOWED_METHODS.contains(method)) {
// 封装 request 和 请求方式
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
所以, 我们如果想要使用 RestFul 风格, 除了要开启 spring.mvc.hiddenmethod.filter.enabled, 还要在表单提交的时候做一些调整, 其实从表单的设置能看出来为什么配置中带有 hiddenmethod 啦。
<!-- post, get 请求方式不变 -->
<form action="/user" method="get">
<input value="REST-GET" type="submit"/>
</form>
<form action="/user" method="post">
<input value="REST-POST" type="submit"/>
</form>
<!-- put, delete 请求时使用 post 方式提交 -->
<form action="/user" method="post">
<!-- 使用一个隐藏的输入框设置 _method -->
<input name="_method" value="put" type="hidden"/>
<input value="REST-PUT" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" value="delete" type="hidden"/>
<input value="REST-DELETE" type="submit"/>
</form>
当表单发起请求时, 首先会经过这个 HiddenHttpMethodFilter 过滤器(如果开启了), 经过调整后会将原生的 request 进行封装, 然后再交给 Controller 层。
注意:如果进行前后端分离了, 那这个实际就用不到了, 前端设置请求方式是 PUT 那就会映射到 @PutMapping
拓展:原来 name="_method",这里如果想自己设置一个名字,比如 name="_m", 可使用如下方式, 自己向容器中加入一个 HiddenHttpMethodFilter
package com.lh.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");//仅仅改了一个参数,功能什么的都没变
return methodFilter;
}
}
请求映射原理
Crtl+H 查看继承树 Crtl + 12查看类方法

SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet 的 doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
// 检测是否为文件上传请求
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 找到当前请求使用哪个Hander (Controller的方法) 处理
mappedHandler = this.getHandler(processedRequest);
// HandlerMapping: 处理器映射 /xxx->>xxx
我们进入 mappedHandler = this.getHandler 查看方法
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
// 由 RequestMappingHandlerMapping, WelcomePageHandlerMapping 顺序
// 可知如果 requestMapping 匹配到了, 就不会去欢迎页了
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

默认六种 handlerMapping(Springboot2中没有第一个 RouterFunctionMapping, 不知道干嘛的),一一去遍历看谁能对应请求去处理。
先去找RequestMappingHandlerMapping(controller中的地址映射),如果找到就使用这个,否则继续下一个
RequestMappingHandlerMapping: 保存了所有 @RequestMapping 和 handler 的映射规则。
其中的 mappingRegistry 对所有的请求进行了注册,都有一个映射关系。都可以在这里面看到

WelcomePageHandlerMapping: 欢迎页的映射规则

所有的请求映射都在 HandlerMapping 中。
-
SpringBoot 自动配置欢迎页的 WelcomePageHandlerMapping 。例如访问 / 能访问到 index.html;
-
SpringBoot 自动配置了默认的 RequestMappingHandlerMapping
-
请求进来,挨个尝试所有的 HandlerMapping 看是否有请求信息。
-
如果有就找到这个请求对应的 handler
-
如果没有就是下一个 HandlerMapping
-
-
如果我们需要一些自定义的映射处理,我们也可以自己给容器中放 HandlerMapping。自定义 HandlerMapping, 但是目前还用不到哈
⚠️ 路径参数与 param 参数小细节
当我们有两个如下请求时
@GetMapping("/commodityInfo")
public Result getCommodityInfo(@RequestParam("userId") Integer userId, @RequestParam("communityId") String communityId){
...
}
@GetMapping("/commodityInfo")
public Object getCommodityInfoPage(@RequestParam("communityId") String communityId, @RequestParam("page") Integer page, @RequestParam("pageSize") Integer pageSize){
...
}
运行是会报错的,即使参数列表不一样, 因为它是先去 handlerMapping 中找映射, 根据的就是请求方式和 @...Mapping 中定义的URL, 所以它会报错说 /commodityInfo 在 RequestMappingHandlerMapping 中已经注册过了。
但是如果把第二个映射改成如下就没有问题
@GetMapping("/commodityInfo") // 第一个不变
public Result getCommodityInfo(@RequestParam("userId") Integer userId, @RequestParam("communityId") String communityId){
...
}
@GetMapping("/commodityInfo/{communityId}/{page}/{pageSize}") // 第二个使用路径传参
public Object getCommodityInfoPage(@PathVariable("communityId") String communityId, @PathVariable("page") Integer page, @PathVariable("pageSize") Integer pageSize){
...
}
参数是绑定在 url 请求路径上的, 所以他们的 handler 是不一样的!!
3.3.2 普通参数与基本注解
注解
@PathVariable路径变量
// 源码注释
If the method parameter is Map<String, String> then the map is populated with all path variable names and values.
如果参数中有 Map<String, String> 类型, 那么所有的路径变量名和值都会放在这里面
Controller 测试
@GetMapping("/user/{id}/name/{username}")
public Map<String, Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String, String> pv){
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", name);
map.put("pv", pv);
return map;
}
请求 localhost:8080/user/1/name/dog
{"pv":{"id":"1","username":"dog"},"name":"dog","id":1}
@RequestHeader获取请求头
If the method parameter is Map<String, String>, MultiValueMap<String, String>, or HttpHeaders then the map is populated with all header names and values.
@GetMapping("/user/{id}/name/{username}")
public Map<String, Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String, String> pv,
// 请求头信息
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String, String> header){
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", name);
map.put("pv", pv);
map.put("userAgent", userAgent);
map.put("header", header);
return map;
}
访问 URL localhost:8080/user/1/name/dog
{"pv":{"id":"1","username":"dog"},"name":"dog","header":{"host":"localhost:8080","connection":"keep-alive","cache-control":"max-age=0","sec-ch-ua":"\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Windows\"","upgrade-insecure-requests":"1","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","sec-fetch-site":"none","sec-fetch-mode":"navigate","sec-fetch-user":"?1","sec-fetch-dest":"document","accept-encoding":"gzip, deflate, br, zstd","accept-language":"zh-CN,zh;q=0.9","cookie":"Webstorm-81e12ffa=0e64ceb3-4834-44fa-a66c-037117f0f3f7; Pycharm-36ac84af=ee9dc67f-2267-4cb9-88f9-c4ced772c81c; Idea-60be9b34=1701f927-adc8-4c89-80f3-287c15bbcec3"},"userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36","id":1}
@RequestParam获取请求参数
If the method parameter is Map<String, String> or MultiValueMap<String, String> and a parameter name is not specified, then the map parameter is populated with all request parameter names and values
@GetMapping("/user")
public Map<String, Object> getCar(@RequestParam("id") Integer id,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String, String> params,
@RequestParam MultiValueMap<String, String> params2){
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("inters", inters);
map.put("params", params);
map.put("params2", params2);
return map;
}
URL 请求 http://localhost:8080/user?id=1&inters=%E5%90%83%E9%A5%AD&inters=%E7%9D%A1%E8%A7%89&inters=%E5%86%99%E4%BB%A3%E7%A0%81
{"inters":["吃饭","睡觉","写代码"],"params2":{"id":["1"],"inters":["吃饭","睡觉","写代码"]},"id":1,"params":{"id":"1","inters":"吃饭"}}
对比 params 和 params2, 如果一个属性有多个值, 需要使用 MultiValueMap
@MatrixVariable矩阵变量
用不到, 暂时知道, 矩阵变量需要绑定在路径变量中, 页面开发,如果 cookie 被禁用了,session 里面的内容使用该方式
CookieValue获取 cookie 值
@RequestBody获取请求体
页面表单
<form action="/save" method="post">
<input type="text" name="name"/>
<input type="text" name="age" value="20"/>
<input type="submit">submit</input>
</form>
handler 定义
@PostMapping(value = "/save",produces = "application/json;charset=utf-8")
public Map postMethod(@RequestBody String content) {
Map<String,Object> map = new HashMap<>();
System.out.println(content);// 中文乱码 使用JSON数据传输就不会出错,直接提交会出问题
map.put("content",content);
return map;
}
{"content":"name=%E4%BA%8C%E7%8B%97&age=20"}
解决方法:接收 JSON 形式的数据并以 JSON 格式返回是最好的方式
3.4 thymeleaf
不做重点, 大致看看, 前后端分离用不到这个
3.4.1 自动配置
@AutoConfiguration(after = { WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@Import({ TemplateEngineConfigurations.ReactiveTemplateEngineConfiguration.class,
TemplateEngineConfigurations.DefaultTemplateEngineConfiguration.class })
public class ThymeleafAutoConfiguration {}
- 所有 thymeleaf的配置值都在 ThymeleafProperties
- 配置好了 SpringTemplateEngine
- 配好了 ThymeleafViewResolver
- 我们只需要直接开发页面
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; // 页面放在templates就行
......
}
3.4.2 构建后台管理系统
@Controller
public class IndexController {
/**
* 来登录页
* @return
*/
@GetMapping(value={"/","/login"})
public String loginPage(){
return "login";
}
@PostMapping("/login")
public String main(User user, HttpSession session, Model model){
// 登录成功,重定向到main.html;防止表单重复提交-->使用重定向
// 把登录成功的用户保存起来
if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
session.setAttribute("loginUser",user);
}else{
model.addAttribute("msg","账号密码错误");
return "login";
}
// return "main";
return "redirect:/main.html";
}
@GetMapping("/main.html")
public String mainPage(HttpSession session,Model model){
// 判断是否登录。 应使用拦截器,过滤器
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return "main";
}else{
// 回到登录页面
model.addAttribute("msg","请重新登录");
return "login";
}
}
}
- 当用户是请求的
/与/login时,显示url为请求的(因为没有重定向),只是返回了login页面。 - 用户在login页面使用 post方式请求
/login时,携带了信息,在此处进行判断用户登录信息。
return 语句对页面会有影响。- 当
return "main";时,页面url仍为/login,刷新后会重新提交表单,即重新使用post方式请求/login,这时会出现提示,是否重新提交表单,需要点击,很麻烦。 - 为
return "redirect:/main.html"时,即进行重定向,此时 url 为/main.html,刷新页面后,是去请求/main.html就不会提交表单了。
- 当
href和th:href的区别
在默认项目路径为空时,打Jar包单独运行时。二者效果一致。
在使用Maven内嵌Tomcat或打War包部署到Servlet容器,或者在项目内执行App启动类,且有配置项目路径时。
二者区别如下:(这一点很重要)
href始终从端口开始作为根路径:如-http://localhost:8080/aa/bb
th:href会寻找项目路径作为根路径:如-http://localhost:8080/myProject/aa/bb
一般都用 th:href
3.5 拦截器
同 SpringMVC 中使用的一样
例如我们进行一个登录检查
编写拦截器
public class LoginInterceptor implements HandlerInterceptor {
/**
* 在 preHandle 中拦截
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登录后用户的信息一般都要存在 session 中
HttpSession session = request.getSession();
// 看是否有 loginUser 的信息
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
// 已登录, 就放行
return true;
}
// 未登录, 带上反馈信息
// 重定向的路径
response.sendRedirect("/?msg=请先进行登录");
return false;
}
}
这里由于是继承的 HandlerInterceptor, 返回值是 boolean 类型, 所以我们不能返回视图或数据, 只能通过原生的 request, response 进行转发或重定向, 下面说一下转发和重定向如何携带数据
带上 request.getContextPaht() 是为了获取上下文, 带上之后转发或重定向不会出错(好像是部署到服务器之后和项目路径有关, 带上之后就不会因为这个出现问题)
// 转发
request.setAttribute("msg", "");
request.getRequestDispatcher(request.getContextPath() + "url").forward(request, response);
// 重定向
response.sendRedirect(request.getContextPath() + "/index.jsp?msg=" + msg);
不是很常用, 因为拦截一般也不会携带大量数据, 用到再查吧
配置拦截器, 在 com.lh.config 包下, SpringBoot 自动包扫描机制
/**
* 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/**","/js/**","/fonts/**","/images/**");
}
}
3.6 文件上传
🐴 文件上传大小配置
默认值(MultipartProperties.java)
// Whether to enable support of multipart uploads.
private boolean enabled = true;
// Max file size.
private DataSize maxFileSize = DataSize.ofMegabytes(1); // 1MB
// Max request size.
private DataSize maxRequestSize = DataSize.ofMegabytes(10); // 10MB
配置, 图片一般都超过 1MB, 所以此处要设置一下
spring:
servlet:
multipart:
enabled: true # 开启文件上传, 默认是开启的
max-file-size: 20MB # 单文件最大限制
max-request-size: 200MB # 一次请求上传所有文件的最大限制. 如果接口只支持单文件,则该值与上面相同即可
这里就将图片文件上传到服务器本地和阿里云 OSS 服务器, 其他的还没有学到。
3.6.1 上传到服务器本地
注意要先进行文件配置
在 template 文件夹下书写页面
<form method="post" action="/image/localUpload" enctype="multipart/form-data">
<input type="file" name="image"/>
<input type="submit" value="提交"/>
</form>
enctype 必须要写为文件上传
@PostMapping("/localUpload")
public String localUpload(@RequestParam("image") MultipartFile file) throws IOException {
// file 校验, 还可以检测格式, 是否为 png, jpg 等等
if(file.isEmpty()){
return "图片上传失败";
}
// file 重命名, 一个 uuid + 后缀名
String originalFilename = file.getOriginalFilename();
String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString().replace("-", "") + "." + ext;
// 上传地址
ApplicationHome applicationHome = new ApplicationHome(this.getClass());
String pre = applicationHome.getDir().getParentFile().getParentFile().getAbsolutePath() + "\\src\\main\\resources\\static\\image\\";
// 完整路径
String path = pre + fileName;
// 上传
file.transferTo(new File(path));
return path;
}
代码:applicationHome.getDir().getParentFile().getParentFile().getAbsolutePath() + "\\src\\main\\resources\\static\\image\\";

applicationHome.getDir() 是 target 下的 classes
第一个 getParentFile() 是获取的 target
第二个 getParentFile() 是获取的 整个项目的根 springboot-base-springmvc-03, 也即是 springboot-part 的一个子模块。
最终存储位置放在 resource 目录下的 static/image 文件夹下
上传后返回地址, 我们再通过地址访问

缺点:目前大部分企业都是分布式(不是很理解, 大致如下), 即服务器有多台, 当某个人第一次访问时, 是其中一台, 上传的文件保存到该服务器本地, 下一次访问可能就是另一台, 上传的文件就找不到了, 所以还是要使用 OSS 对象存储的方式比较好, 生成链接, 有个专门的服务器放文件。
3.6.2 OSS 对象存储
参考 https://juejin.cn/post/7040070983444594696
导入阿里云依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.5.0</version>
</dependency>
<!-- 下面两个主要用一个工具类的方法, 不导入自己写也可以 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
咱也整点骚的, 使用 yaml 配置
# 自定义名字
aliyun:
access-id: LTAI5tKpLpiXeRoymsKcN672
access-key: wxsckY98x1sud5YEQbxkZe0nQ7Jef4
bucket: typora-picture-0 # 自己使用的名字
endpoint: oss-cn-beijing.aliyuncs.com
配置 OSSProperties
@Component
@Data
@ConfigurationProperties(prefix = "aliyun")
public class OSSProperties implements InitializingBean {
private String endpoint;
private String accessId;
private String accessKey;
private String bucket;
// 声明静态变量
public static String ALI_ENDPOINT;
public static String ALI_ACCESS_ID;
public static String ALI_ACCESS_KEY;
public static String ALI_BUCKET;
@Override
public void afterPropertiesSet() { // 当 bean 初始化完成后, 值存一份在静态变量中
ALI_ENDPOINT = endpoint;
ALI_ACCESS_ID = accessId;
ALI_ACCESS_KEY = accessKey;
ALI_BUCKET = bucket;
}
}
配置 OSSUtil
public class OSSUtil {
private static final String ENDPOINT = OSSProperties.ALI_ENDPOINT;
private static final String ACCESS_KEY_ID = OSSProperties.ALI_ACCESS_ID;
private static final String ACCESS_KEY_SECRET = OSSProperties.ALI_ACCESS_KEY;
private static final String BUCKET_NAME = OSSProperties.ALI_BUCKET;
// 整体域名
private static final String ALI_DOMAIN = "https://" + BUCKET_NAME + "." + ENDPOINT + "/";
/**
* 删除单个图片
*/
public static void deleteImg(String url) {
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
// https://typora-picture-0.oss-cn-beijing.aliyuncs.com/56504048dbbf4b0d8d99a823969ebcfa.JPG
String fileName = url.substring(url.lastIndexOf("/") + 1);
ossClient.deleteObject(BUCKET_NAME, fileName); // 给 bucket 名和文件名即可
ossClient.shutdown();
}
/**
* 上传图片
*/
public static String uploadImg(MultipartFile file) throws IOException {
// file 校验
if(file.isEmpty()){
return "图片上传失败";
}
// file 重命名, 一个 uuid + 后缀名
String originalFilename = file.getOriginalFilename();
// 用的 commons.io 中的工具库, 获取后缀名
String ext = "." + FilenameUtils.getExtension(originalFilename);
String fileName = UUID.randomUUID().toString().replace("-", "") + ext;
// OSS 客户端对象
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
// 桶名, 文件名
ossClient.putObject(BUCKET_NAME, fileName, file.getInputStream());
ossClient.shutdown();
return ALI_DOMAIN + fileName; // 图片 URL 地址
}
}
前端页面编写, 记得开启 RestFul 风格支持
<form method="post" action="/image" enctype="multipart/form-data">
<input type="file" name="image"/>
<input type="submit" value="提交"/>
</form>
<form method="post" action="/image">
<input type="hidden" name="_method" value="delete"/>
<input type="text" name="imageUrl" placeholder="url"/>
<input type="submit" value="确认删除"/>
</form>
控制层
@RequestMapping("/image")
@RestController
public class ImageController {
@PostMapping
public String upload(@RequestParam("image") MultipartFile file) throws IOException {
return OSSUtil.uploadImg(file);
}
@DeleteMapping
public void delete(@RequestParam("imageUrl")String url){
OSSUtil.deleteImg(url);
}
}
已测试, 添加、删除均能正常工作
3.7 异常处理
-
默认情况下,Spring Boot 提供
/error处理所有错误的映射 -
对于机器客户端(如Postman),它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个 whitelabel 错误视图,以 HTML 格式呈现相同的数据, 比如如下请求一个不存在的映射

JSON数据timestamp 2022-11-22T05:53:28.416+00:00 status 404 error Not Found path /dog/ergou/shabi 可以在页面中使用thymeleaf取值
${message}<h3 th:text="${message}">Something went wrong.</h3> <p class="nrml-txt" th:text="${trace}">Why not try refreshing you page? Or you can <a href="#">contact our support</a> if the problem persists.</p> -
要完全替换默认行为,可以实现
ErrorController并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制替换其内容(太高级) -
error文件夹下的 4xx,5xx页面会被自动解析(记这个)


比如我们在 templates.error 自定义一个 4xx 页面

其他都不变, 重新请求 URL localhost:8080/dog/ergou/shabi

由此可以实现自定义 error 页面
3.8 Web原生组件注入(Servlet、Filter、Listener)
原生组件不是很了解, 暂时记录
1. 使用Servlet API(推荐使用)
放在主程序类上
// 指定原生Servlet放在了哪里
@ServletComponentScan(basePackages = "com.lh.admin")
@SpringBootApplication
public class Boot05AdminApplication {
}
Enables scanning for Servlet components (filters, servlets, and listeners). Scanning is only performed when using an embedded web server.
Typically, one of value, basePackages, or basePackageClasses should be specified to control the packages to be scanned for components. In their absence, scanning will be performed from the package of the class with the annotation.
使用原生的servlet,不会触发Spring的拦截器
扩展:DispatchServlet 如何注册进来
- 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
- 通过
ServletRegistrationBean<DispatcherServlet>把 DispatcherServlet 配置进来。 - 默认映射的是 / 路径。

原来容器中有一个DispatcherServlet,在里面执行 applyPostHandle applyPreHandle 等拦截器的方法,而自己注册的MyServlet中没有,也就不能执行拦截器。 默认只有一个DispatcherServlet,自己注册后数量变化,处理的类不一样(一个Spring,一个Tomcat)
🔘 Tomcat-Servlet:
多个Servlet都能处理同一层路径,精确优选原则
A: /
B: /my
/my --> (A,B均能处理该请求,但B更精准)来到 B
Servlet
@WebServlet(urlPatterns = "/my") // 路径
public class MyServlet extends HttpServlet { // implements也可以
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("666666");
}
}
Filter
@Slf4j
@WebFilter(urlPatterns = {"/css/*","/fonts/*","/images/*"}) // 单星表示所有是Servlet的写法,双星表示所有是Spring的写法
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("doFilter");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
log.info("destroy");
}
}
Listener
@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. 使用RegistrationBean
当不太复杂时,可以使用这种方式
If convention-based mapping is not flexible enough, you can use the
ServletRegistrationBean,FilterRegistrationBean, andServletListenerRegistrationBeanclasses for complete control.
上面的类定义稍加修改, 即不需要使用 @WebFilter、@WebServlet、@WebListener 注解, 也不需要指定扫描了, 因为 @Configuration 可以被自动扫描到。
Servlet
public class MyServlet extends HttpServlet { // implements也可以
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("666666");
}
}
Filter
@Slf4j
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("doFilter");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
log.info("destroy");
}
}
Listener
@Slf4j
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁");
}
}
@Configuration
public class MyRegisterConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean<>(myServlet,"/my","/my2");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
//return new FilterRegistrationBean<>(myFilter,myServlet());
FilterRegistrationBean<MyFilter> filterRegistrationBean = new FilterRegistrationBean<>(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MyServletContextListener myServletContextListener = new MyServletContextListener();
return new ServletListenerRegistrationBean<>(myServletContextListener);
}
}
MyFilter MyServlet MyServletContextListener 只作为普通类,不加@Web**注解, MyFilter 和 MyServlet 中注解里面需要设置的内容可以由 RegistrationBean 进行设置

4. 数据库整合
4.1 SQL
4.1.1 Druid 连接池
目前, SpringBoot3 还没和 druid 进行完整对接, 所以需要较多配置进行兼容性处理
导入依赖
<dependencies>
<!-- web开发的场景启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 数据库相关配置启动器 jdbctemplate以及事务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- druid 启动器的依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.20</version>
</dependency>
<!-- 驱动类 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
在 application.yaml 中配置连接池信息
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 使用 druid 连接池
druid:
username: root
password: mysql
url: jdbc:mysql://localhost:3306/mybatis-example
driver-class-name: com.mysql.cj.jdbc.Driver
# 初始化时建立物理连接的个数
initial-size: 5
# 连接池的最小空闲数量
min-idle: 5
# 连接池最大连接数量
max-active: 20
# 获取连接时最大等待时间, 单位毫秒
max-wait: 60000
# 申请连接的时候检测, 如果空闲时间大于 timeBetweenEvictionRunsMillis, 执行 validationQuery 检测连接是否有效
test-while-idle: true
# 即作为检测的间隔时间又作为 test-while-idle 执行的依据
time-between-eviction-runs-millis: 60000
# 销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时, 关闭当前连接(配置连接在池中的最小生存时间)
min-evictable-idle-time-millis: 30000
# 用来检测数据库连接是否有效的 sql, 必须是一个查询语句
validation-query: select 1
# 申请连接时会执行 validation-query 检测连接是否有效, 开启会降低性能, 默认为 true
test-on-borrow: false
# 归还连接时会执行 validation-query 检测连接是否有效, 开启会降低性能, 默认为 true
test-on-return: false
# 是否缓存 prepared-statement, 也就是 PSCache, PSCache 对支持游标的数据库性能提升巨大, 比如 oracle, 在mysql中建议关闭
pool-prepared-statements: false
# 要启用 PSCache, 必须配置大于0, 当大于0时, poolPreparedStatements自动触发修改为true
max-pool-prepared-statement-per-connection-size: -1
# 合并多个 druidDataSource 的监控数据
use-global-data-source-stat: true
测试:实体类 Student, 有属性 sId, sName
@RestController
@RequestMapping("/student")
public class UserController {
// 在导入 jdbc 场景的时候, jdbcTemplate 已将加入到了容器中, 所以直接使用就可以了
@Autowired
private JdbcTemplate jdbcTemplate;
@GetMapping("/list")
public List<Student> list(){
String sql = "select * from student;";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Student.class));
}
}
访问 http://localhost:8080/student/list, 可以正确查询到数据
[{"sname":"dog","sid":1}]
注意:druid 连接池场景启动器如果使用 1.2.20 之前的版本会出现文件。不过现在 1.2.20 版本之后到这里就可以正常使用了
运行--------出现错误 Caused by: java.lang.ClassNotFoundException: io.r2dbc.spi.ValidationDepth
原因:SpringBoot3 与 Druid 的兼容性问题, 因为 SpringBoot2 时是会自动加载 druid 连接池, 但是 3 版本后, 加载方式变了, druid 还没有进行整改, 所以需要我们手动处理一下
在 resources 创建文件夹 META-INF/spring, 创建文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件内容如下
com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure

这样让 SpringBoot 去加载自动配置
4.1.2 Mybatis 整合
原理简介
MyBatis-Spring-Boot-Starter 将会:
- 自动探测存在的
DataSource(这里是 druid) - 将使用
SqlSessionFactoryBean创建并注册一个SqlSessionFactory的实例,并将探测到的DataSource作为数据源 - 将创建并注册一个从
SqlSessionFactory中得到的SqlSessionTemplate的实例 - 自动扫描你的 mapper,将它们与
SqlSessionTemplate相关联,并将它们注册到Spring 的环境(context)中去,这样它们就可以被注入到你的 bean 中
MyBatis-Spring-Boot-Starter 将默认搜寻带有 @Mapper 注解的 mapper 接口。
你可能想指定一个自定义的注解或接口来扫描,如果那样的话,你就必须使用 @MapperScan 注解了。这样其他的接口就可以不用标注 @Mapper 注解。
@SpringBootApplication
@MapperScan("com.lh.mapper") // 包下面所有的类都认为是mapper接口
public class Main{
...
}
要么在所有的 mapper 接口上加上 @Mapper
@Mapper
public interface StudentMapper{
...
}
要么指定 主程序上加上 @MapperScan
整合过程
在 4.1.1 的基础上加上
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
配置连接池及 mybatis 配置信息
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 使用 druid 连接池
druid:
username: root
password: mysql
url: jdbc:mysql://localhost:3306/mybatis-example
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
# mapper 配置文件所在位置
mapper-locations: classpath:/mappers/*.xml
type-aliases-package: com.lh.pojo
# settings 在 configuration 里面配置即可
configuration:
map-underscore-to-camel-case: true
auto-mapping-behavior: full
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
创建 mapper 接口
public interface StudentMapper {
List<Student> queryList();
}
创建 mapper 配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lh.mapper.StudentMapper">
<select id="queryList" resultType="student">
select * from student;
</select>
</mapper>
对于比较简单的SQL语句, 可以不在 xml 配置中写, 而直接在 mapper 接口上使用注解的方式
public interface StudentMapper {
@Select("SELECT * from student")
List<Student> queryList();
@Insert("INSERT INTO Student(s_name) value(#{name})")
// 属性的配置都在@Options 可以看源码写
@Options(useGeneratedKeys = true,keyProperty = "id")
void insert(@Param("name") String name);
}
推荐做法:比较简单的语句使用注解, 较复杂的语句使用XML配置文件, 二者可以同时存在, 但是不能同一个方法即用注解写了SQL语句, 又用配置文件写了
ResultMap 需要在 xml 文件中配置好,如果使用注解可以用 @ResultMap(name = "") 但是 ResultMap 配置还是要在xml文件中, 所以嘞, 复杂的语句都写着 XML 里面, 或者所有语句都写在 XML 里面也可以, 结构比较清晰(个人认为)
在 Controller 层设置访问地址
@RestController
@RequestMapping("/student")
public class UserController {
@Autowired
private StudentMapper studentMapper;
@GetMapping("/list")
public List<Student> list(){
return studentMapper.queryList();
}
}
在主程序中加入注解 @MapperScan
@MapperScan("com.lh.mapper")
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
🐯 @MapperScan 指定的是定义的 mapper 接口, 扫描后会加入容器, 我们在使用 StudentMapper 的地方直接注入就可以了
🐯 在 yaml 配置文件中指定的是 mapper 配置文件所在位置
运行访问即可
整合 pageHelper
依赖引入
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
调用, 结合 PageBean 简化数据传输
package com.lh.idlestore.utils.pojo;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean<T>{
// 当前页码
private int currentPage;
// 每页显示的数据量
private int pageSize;
// 总数据条数
private long total;
// 当前页的数据集合
private List<T> data;
}
Controller 使用
@GetMapping("/commodityInfo/{communityId}/{page}/{pageSize}")
public Result getCommodityInfoPage(@PathVariable("communityId") String communityId, @PathVariable("page") Integer page, @PathVariable("pageSize") Integer pageSize){
// 分页查询
PageHelper.startPage(page, pageSize);
List<Commodity> commodityList = commodityService.list();
PageInfo<Commodity> commodityPageInfo = new PageInfo<>(commodityList);
// 使用 pageBean 类减少不必要的数据传输
PageBean<Commodity> commodityPageBean = new PageBean<>(page, pageSize, commodityPageInfo.getTotal(), commodityPageInfo.getList());
// 使用 Result
return Result.ok(MessageEnum.MESSAGE_COMMODITY_PAGE_QUERY_SUCCESS, commodityPageBean);
}
4.1.3 Mybatis-plus 整合
Mybatis-plus 的理念就是只在 Mybatis 上做增强, 不做修改, 加入 Mybatis-plus 不会影响 Mybatis 的使用
这里只做简单整合, 因为原来学 mybatis-plus 是使用的 springboot2, 所以会将 mybatis-plus 笔记里的再使用 springboot3 验证一遍。详情看 mybatis-plus。
🎠 依赖引入
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 引入这个包, 包含了 mybatis 和 jdbc -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- 这里还是使用 druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.20</version>
</dependency>
application.yaml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis-example?characterEncoding=utf-8&useSSL=false
username: root
password: mysql
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 日志输出功能,StdOutImpl是内置的
# 默认开启驼峰映射
数据库表
CREATE TABLE `user` (
`id` bigint NOT NULL, # 注意这里并没有让它自增, 因为我们要使用雪花算法
`name` varchar(30) NULL DEFAULT NULL,
`age` int NULL DEFAULT NULL,
`email` varchar(50) NULL DEFAULT NULL,
PRIMARY KEY (`id`)
)
创建 pojo 类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
// 默认雪花算法生成id, 必须为Long类型
private Long id;
private String name;
private Integer age;
private String email;
}
mapper 层继承接口 BaseMapper<T>
public interface UserMapper extends BaseMapper<User> {
}
创建主程序
@MapperScan("com.lh.mapper")
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
测试类
@SpringBootTest(classes = Application.class) // 需指定主程序类
public class MybatisPlusTest {
// 注入 UserMapper
@Autowired
private UserMapper userMapper;
@Test
public void testSelectList(){
// SELECT id,name,age,email FROM user
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
}
至此我们已经讲完了基本用法, 可以进行数据库操作了, 但是我们的程序都是三层架构, userMapper 注入成功了, service 层可以访问 mapper 了, 但是我们如何让 controller 层可以调用 service 层来使用这些方法呢?
IUserService 继承 IService<>
public interface IUserService extends IService<User> {
// 在接口中自定义方法
}
service 层继承接口 ServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{
// 可以使用 userMapper 啦
@Autowired
private UserMapper userMapper;
}
controller 层
@Controller
@RequestMapping("/user")
public class UserController{
@Autowired // 注入 UserServiceImpl
private IUserService userService;
}
进而在 controller 层也可以调用了。
❓ 有点复杂, 没看懂结构, 讲解一下
- mybatis-plus 的理念就是只做增强不做修改
userMapper层继承了BaseMapper<T>, 相当于继承了里面的方法, 由 mybatis-plus 帮我们生成简单的 crud 语句, 这里有些细节, 我们原来自己定义 SQL 语句的时候会指定表名和字段名, 那么 mybatis-plus 如何做的呢?它会根据我们的 pojo 实体类名和字段名进行映射(mybatis-plus笔记里讲自定义映射规则), 所以 mybatis-plus 驼峰映射是自动开启的(mybatis需要手动开启)。
它帮助我们生成接口方法和实现的SQL语句, 如果我们要自定义SQL方法, 那么和原来一样, 接口中定义好方法再去定义mapper.xml 文件, 不过别忘了, 和 mybatis 一样, 我们要设置mapper-locations, 也就是说, 如果你不知道如何写程序, 那就按照 mybatis 的规则就不会错userServiceImpl层继承了ServiceImpl<>这是为了让userMapper层的方法在 service 层有一个体现, 便于 controlelr 层的调用, 同样我们如果要自定义方法, 只需要自定义接口IUserService, 然后implements实现它
对于里面的方法使用以及字段映射、表映射、枚举类、乐观锁、分页插件使用、逻辑删除等都在 mybatis-plus 笔记里面, 还是很实用的。
4.2 杂项
4.2.1 事务整合
注意, 事务是 jdbc 提供的, 它不依赖于 mybatis 等场景, 所以导入 jdbc 的场景就可以使用事务, 当然, 你不导用不了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
SpringBoot 项目会自动配置一个 DataSourceTransactionManager, 所以我们只需要在方法上或者类上加上 @Transactional 注解就可以纳入 Spring 的事务管理了 (细节可以看 Spring 第五章笔记)
@Transactional
public void update(){
Student student = new Student();
student.setsId(1);
student.setsName("二狗");
UserMapper.update(student);
}
⚠️ @Transactional 注解要用到 Service 层里面!!不要在 @Controller 层中使用。
4.2.2 AOP 整合
依赖导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
直接使用 aop 注解即可
@Component
@Aspect
public class LogAspect {
// 前置通知
@Before(value = "execution(* com.lh.spring6.aop.annotation.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
...
}
}
详情见 Spring 第三章节笔记
5. 项目打包和运行
之前的 web 项目, 都需要我们打成 war 包, 然后放在 tomcat 的 webapps 目录下, 当 tomcat 启动时, 里面的 war 包就会自动解压, 外部就可以访问了
但是 SpringBoot3 内置了服务器软件, 我们只需要打成 jar 包, 然后命令执行 java -jar xx 执行内部的服务器软件就可以了
5.1 项目打包
用来支持将项目打包成可执行、可运行的 jar 包
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

5.2 项目运行
java -jar 命令用于在 Java 环境中执行可执行的 JAR 文件
命令格式: java -jar [选项] [参数] <jar文件名>
-
-D<name>=<value>: 设置系统属性, 可以通过System.getProperty()方法在应用程序中获取该属性值。例如java -jar -Dserver.port=8080 myapp.jar -
-X: 设置 JVM 参数, 例如内存大小、垃圾回收策略等-Xmx<size>: 设置 JVM 的最大堆内存大小, 如-Xmx512m表示设置最大堆内存为 512MB-Xms<size>:设置 JVM 的初始堆内存大小, 如-Xmx256m表示设置最大堆内存为 256MB
-
-Dspring.profiles.active=<profile>:指定 SpringBoot 的激活配置文件, 可以通过application-<profile>.properties或application-<profile>.yml文件来加载相应的配置。例如:java -jar -Dspring.profiles.active=dev myapp.jar(结合 2.3 节笔记)
我们在 jar 包所在的位置输入 cmd 运行

然后在浏览器就可以访问了

按 Ctrl + C 可以停掉服务
6. 问题解决
6.1 枚举类型返回前端
当我们在 Java 程序中定义枚举类后, 当对象中有一个属性为该枚举类时, 前端是看不懂的, 比如我定义如下枚举类
@Getter
public enum StatusEnum {
PENDING_REVIEW(1, "待审核"),
ON_SALE(2, "在售中"),
SOLD_OUT(3, "已售空");
@EnumValue
private final Integer status;
private final String statusName;
StatusEnum(Integer status, String statusName) {
this.status = status;
this.statusName = statusName;
}
}
实体类中有属性
private StatusEnum status;
返回前端后, 它的实际值为
PENDING_REVIEW 或者 ON_SALE 或者 SOLD_OUT, 而我们实际想要的是 code + message, 那么如何获取?
实现步骤
添加注解
@Getter
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum StatusEnum implement BaseEnum{
PENDING_REVIEW(1, "待审核"),
ON_SALE(2, "在售中"),
SOLD_OUT(3, "已售空");
@EnumValue
private final Integer status;
private final String statusName;
StatusEnum(Integer status, String statusName) {
this.status = status;
this.statusName = statusName;
}
}
将枚举类以对象形式返回, 比如我不加注解返回值为
"status": "PENDING_REVIEW"
加上之后
"status": {
"status": 1,
"statusName": "待审核"
}
前端向后端传递的时候应该传递 字段名 = 下标 比如我想更新状态为待审核, 则应该传递值 0, 而不是1, 因为这个0代表枚举类中定义的顺序。待审核位于第一个, 应该传递0。
913

被折叠的 条评论
为什么被折叠?



