目录
[1. 配置文件](#1. 配置文件)
[1.1 什么是配置文件](#1.1 什么是配置文件)
[1.2 配置文件存放的内容](#1.2 配置文件存放的内容)
[1.3 配置文件的作用](#1.3 配置文件的作用)
[2. SpringBoot 配置文件](#2. SpringBoot 配置文件)
[3. properties](#3. properties)
[3.1 读取配置文件](#3.1 读取配置文件)
[3.1.1 @Value](#3.1.1 @Value)
[3.1.2 @PostConstruct](#3.1.2 @PostConstruct)
[3.2 properties 缺点](#3.2 properties 缺点)
[4. yml](#4. yml)
[4.1 yml 基本语法](#4.1 yml 基本语法)
[4.2 yml 配置数据类型](#4.2 yml 配置数据类型)
[4.2.1 yml 读取配置](#4.2.1 yml 读取配置)
[4.3 yml 配置对象](#4.3 yml 配置对象)
[4.3.1 读取对象数据](#4.3.1 读取对象数据)
[4.2 yml 配置集合](#4.2 yml 配置集合)
[4.2.1 配置 List & Map](#4.2.1 配置 List & Map)
[5. 综合练习](#5. 综合练习)
[5.1 验证码案例](#5.1 验证码案例)
[5.1.1 需求](#5.1.1 需求)
[5.1.2 接口文档](#5.1.2 接口文档)
[5.1.2.1 生成验证码](#5.1.2.1 生成验证码)
[5.1.2.2 校验用户输入](#5.1.2.2 校验用户输入)
[5.1.3 接口一: 生成验证码](#5.1.3 接口一: 生成验证码)
[5.1.4 接口二: 校验用户输入](#5.1.4 接口二: 校验用户输入)
[5.1.5 后端代码](#5.1.5 后端代码)
[5.1.6 前端代码](#5.1.6 前端代码)
1. 配置文件
1.1 什么是配置文件
其实, 我们一直都在接触配置文件, 例如一个项目中的 .idea 文件夹中的文件, 就是一个配置文件:

这些配置文件中包含了 IDEA 的背景色, 字体大小, Maven 仓库的地址, ....... 每个项目都有自己的配置文件.
包括我们电脑中的 user 文件夹, 里面也有配置文件:

1.2 配置文件存放的内容
上文说到, 每个项目都有不同的设置, 因此具有不同的配置文件.
那么, 对于用户来说也是一样的, 为了满足每个用户可能存在的不同需求, 我们将可能发生变化的信息, 从代码中分离出来, 放置在配置文件中, 实现和代码的解耦, 以便在不修改代码的情况下, 根据用户的偏好或需求进行调整, 从而解决硬编码问题.
硬编码, 就是指代码写死的, 难以修改的.
1.3 配置文件的作用
因此, 配置文件的作用如下:
- 通过修改配置文件, 可以根据用户需求对程序相关功能进行更改, 而无需修改代码和重新部署, 因此解决了硬编码问题.
2. SpringBoot 配置文件
SpringBoot 配置文件有以下三种(文件名必须为 application, 不可修改):
- application.properties
- application.yml
- application.yaml
properties、yml 和 yaml 不仅仅代表 SpringBoot 的配置文件, 更重要的是它们代表了不同的**文件格式,**每种格式都有自己的语法规则.
其中, yml 就是 yaml 的简写(就像 htm 是 html 的简写一样), 两者代表的是同一个东西.
3. properties
properties 的语法为 key-value 格式, key 和 value 之间使用 = 进行分割. 如:"key = value"
建议: 所有单词小写, 单词之间使用 点(.) 进行分割.
Spring 项目创建时, 就已经帮我们创建了 application.properties 格式的配置文件:

我们可以在配置文件中修改服务器的端口号:

3.1 读取配置文件
3.1.1 @Value
除了程序自带的配置信息外, 我们也可以添加自定义的配置信息:

我们可以通过 @Value 注解将配置信息赋值给属性, 进而读取这些配置信息:

Spring 通过 @Value 拿到配置信息后(初始为字符串类型), 会根据属性的类型, 对字符串进行类型转换.
如果可以转换, 则成功:
如果不可以转换, 则报错:

3.1.2 @PostConstruct
除了上述通过 http 请求来展示配置信息的方式外, 我们还可以通过 @PostConstruct 注解来展示获取到的信息.
@PostConstruct 是一个方法注解.
-
当前类的 Bean 完成实例化 (注意是实例化, 不是初始化)
-
Bean 中所依赖的其他 Bean(属性 Bean) 完成完成注入(初始化)
-
执行 @PostConstruct
实例化: 申请内存空间, 赋予默认值(null / 0 / false 等)
初始化: 把当前类的 Bean 所依赖的其他 Bean(属性 Bean) 完成注入
因此, 在该方法执行时, 当前类 Bean 中的属性 Bean 已经被注入(完成了初始化), 因此不会出现空指针异常的情况.

3.2 properties 缺点
由于 properties 的语法, 使得 properties 类型的配置文件有一个明显的缺点: 配置信息冗余.
例如, 数据库相关的配置文件:

此时, 就需要 yml 出场了.
4. yml
- application.yml 可以和 application.properties 并存在 SpringBoot 项目中, 并且两个配置文件同时生效(取并集).
- 若两个配置文件存在冲突的部分(如: 在两个文件中都修改了端口号), 那么会以 .properties 为准(properties 的优先级更高).
4.1 yml 基本语法
yml 采用树形结构, 基本语法如下图所示:

接下来展示一下 properties 和 yml 的对比:

尤其需要注意的是:
- 第一级不能有空格
- 第一级的 key 名不能重复出现
- 同一级一定要对齐
yml 中其实也可以使用 点(.) 来进行单词间的分割, 就像 properties 形式一样, 但是 key 和 value 一定要使用 冒号(:) 来分割:
4.2 yml 配置数据类型
# 字符串
string:
value: Hello
# 布尔值,true或false
boolean:
value: true
value1: false
# 整数
int:
value: 10
# 浮点数
float:
value: 3.14159
# Null,~代表null
# null 是特殊字符, 需要使用 "" 引起来
"null":
value: ~
# 空字符串(以下几种均表示 空字符串)
empty:
value: ""
value1: ''
value2:
需要额外注意的是 null 和 空字符串.
4.2.1 yml 读取配置
读取 yml 配置文件中的信息, 方式和 properties 是一模一样的.
同样是使用 @Value 将配置信息注入到属性中, 并且单词间使用 点(.) 进行分割:

4.3 yml 配置对象
yml 定义对象, 和我们上面定义数据时的语法格式是相同的:

上图表示的就是, 一个 person 类中, 定义了 id, name, age 三个属性.
4.3.1 读取对象数据
@ConfigurationProperties 是一个类注解和方法注解.
对类使用, 并指定其 prefix 属性, 表示将配置中前缀为 prefix 的信息, 读取到该 Java 类的属性中.

注意:
使用 @ConfigurationProperties 将 JavaBean 中的属性和 配置文件中对象的属性进行匹配时, 两者名字要以一一对应!!
Bean 的生命周期如下:
同样, 也可以使用 @Value 读取配置中对象的某一属性信息:

因此, @Value 和 @ConfigurationProperties 的关系如下:
- 本质都是将配置数据读取到属性的 Bean 中,用于实现配置的注入,且都发生在 Bean 初始化过程中
- @Value: 用于注入单个属性的值. 通常用于注入简单类型的属性, 例如字符串、数字、布尔值等.
- @ConfigurationProperties: 用于将一个配置对象的信息, 注入到整个 Java 类的属性中.
4.2 yml 配置集合
4.2.1 配置 List & Map
在配置文件中配置 List(或数组), Map, 也是以对象的形式来配置的:

读取 List 和 Map 的配置信息, 同样是使用 @ConfigurationProperties:

5. 综合练习
5.1 验证码案例
5.1.1 需求
- 页面生成验证码
- 输入验证码, 点击提交, 验证用户输入验证码是否正确, 正确则进行页面跳转
5.1.2 接口文档
后端需要提供两个接口:
- 生成验证码图片
- 校验用户输入的验证码是否正确
5.1.2.1 生成验证码
请求: (前端给后端服务器发送一个请求)
- URL: /captcha/getCaptcha
响应: 后端生成一个验证码的图片, 显示在页面上
5.1.2.2 校验用户输入
请求 URL: /captcha/check
请求参数: 用户输入的值
响应: 根据用户输入的验证码, 校验验证码是否正确. true: 验证成功. false: 验证失败
5.1.3 接口一: 生成验证码
我们借助开源的 hutool 来生成验证码: Hutool参考文档
当然, 要想使用 hutool, 第一步是先引入 hutool 的 Maven 依赖:

引入 hutool 依赖后, 我们就能够导入 hutool 提供的包, 使用 hutool 所提供的接口了:

了解了 hutool 后, 接下来正式开始接口的定义.
因为我们后端生成的验证码大小, 是可变的, 因此可以将验证码图片的高, 宽, ... 定义在配置文件中:
(注意: 内部类 Session 必须是静态的!!)
并且, 生成验证码后, 我们需要存储两个值:
- 将验证码的正确值, 存储在用户的 Session 中, 以便后续在验证接口中获取并验证用户输入的值是否正确
- 将用户开始填写时(生成验证码完毕时)的时间戳存储到用户 session 中, 以便后续在验证接口中获取并验证用户输入所用时间是否超时

演示:

在这个接口中, 我们需要注意两点:
1. 手动将响应的 Content-Type 设置为 "image/jpeg"
首先, Spring 确实会根据返回的内容自动推测 Content-Type.
如果你返回的是一张图片, Spring 会根据图片的格式(例如 .jpg
, .png
, .gif
等)自动设置合适的 Content-Type.
此外, 浏览器也会有一些智能机制, 自动通过内容来推测 Content-Type.
但是, 为了避免不必要的兼容性问题或误判, 最好还是手动设置 Content-Type, 明确告诉浏览器类型, 确保浏览器按照该类型进行解析.
java
// 手动将响应的 Content-Type 设置为 图片格式, 以便浏览器进行解析
response.setContentType("image/jpeg");
2. 将 get 请求设置为无缓存形式
当我们通过浏览器的地址栏输入 URL 访问一个页面时, 此时的请求方式是 GET 请求.
而浏览器和服务器通常会缓存 GET 请求的响应, 目的是提高页面加载速度并减少不必要的网络请求.
因此, 当用户连续刷新页面时, 返回的都可能是同一个验证码.
取消 get 请求缓存的方式有两种:
- 通过前端取消缓存(通常)
- 通过后端取消缓存
在实际工作中, 通常由前端解决这个问题.
前端解决方式:
html
<!-- 将 get 请求设置为无缓存形式(放在 head 标签中) -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
后端的解决方式:
java
// 设置无缓存形式(通常是由前端处理的)
response.setHeader("Pragma", "no-Cache");
3. 将 Spring 所管理类的内部类设为静态

我们上文说到, Session 必须作为一个静态内部类存在, 接下来解释原因.
如果内部类不是静态的, 那么外部类 Bean 和内部类 Bean 就会形成一个 "死循环", 导致 Spring 无法管理. 原因:
- 要创建外部类的 Bean, Spring 需要先实例化外部类
- 但是,要实例化外部类, Spring 必须先处理其内部类的 Bean(因为可能在外部类中使用了内部类的实例作为字段)
- 由于内部类是非静态的, 所以要实例化内部类的 Bean, Spring 必须先有一个外部类的实例
- 这就形成了一个死循环: 外部类依赖于内部类, 内部类又依赖于外部类, 导致 Spring 无法完成 Bean 的创建
**而静态内部类具有独立性,**静态内部类 (static 修饰) 不依赖于外部类的实例, 它可以像普通类一样独立地创建.
因此, Spring 可以直接实例化静态内部类, 而不会引发循环依赖问题.
结论: 如果一个类交给 Spring 来管理(即作为 Spring Bean), 其中的 内部类 必须是 静态的!!
5.1.4 接口二: 校验用户输入

演示:

5.1.5 后端代码
java
@Data
@Configuration
@ConfigurationProperties(prefix = "captcha")
public class CaptchaProperties {
private int width;
private int height;
private Session session;
@Data
public static class Session {
private String key;
private String date;
}
}
java
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private CaptchaProperties captchaProperties;
// 定义超时时间
private static final long TIME = 30 * 60 * 1000;
@RequestMapping("/getCaptcha")
public void getCaptcha(HttpServletResponse response, HttpSession session) throws IOException {
// 手动将响应的 Content-Type 设置为 图片格式, 以便浏览器进行解析
response.setContentType("image/jpeg");
// response.setCharacterEncoding("utf-8");
// 设置无缓存形式(通常是由前端处理的)
// response.setHeader("Pragma", "no-Cache");
// 定义图形验证码的长、宽、验证码字符数、干扰线宽度
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(captchaProperties.getWidth(), captchaProperties.getHeight());
// 将验证码的值存到用户 Session 中
session.setAttribute(captchaProperties.getSession().getKey(), captcha.getCode());
// 存储验证码生成完毕(用户开始填写)时的时间戳
session.setAttribute(captchaProperties.getSession().getDate(), new Date());
// 直接把验证码写到浏览器
captcha.write(response.getOutputStream());
}
/**
* 验证输入的验证码是否正确
* @param captcha 用户输入的验证码
* @param session
* @return true / false
*/
@RequestMapping("/check")
public boolean check(String captcha, HttpSession session) {
// 从用户 session 中获取验证码的正确值
String code = (String) session.getAttribute(captchaProperties.getSession().getKey());
// 获取用户开始填写时的时间戳
Date date = (Date) session.getAttribute(captchaProperties.getSession().getDate());
// 验证输入值是否合法
if (!StringUtils.hasLength(code)) {
return false;
}
// 忽略大小写, 若输入正确 && 没有超时输入 && session 没有过期(data != null) => 验证通过
if(code.equalsIgnoreCase(captcha) && date != null &&
System.currentTimeMillis() - date.getTime() < TIME) {
return true;
}
// 验证失败
return false;
}
}
java
spring:
application:
name: spring-captcha-demo
captcha:
width: 150
height: 50
session :
key: CAPTCHA_SESSION_KEY
date: CAPTCHA_SESSION_DATE
5.1.6 前端代码

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- 将 get 请求设置为无缓存形式 -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>验证码</title>
<style>
#inputCaptcha {
height: 30px;
vertical-align: middle;
}
#verificationCodeImg{
vertical-align: middle;
}
#checkCaptcha{
height: 40px;
width: 100px;
}
</style>
</head>
<body>
<h1>输入验证码</h1>
<div id="confirm">
<input type="text" name="inputCaptcha" id="inputCaptcha">
<img id="verificationCodeImg" src="/captcha/getCaptcha" style="cursor: pointer;" title="看不清?换一张" />
<input type="button" value="提交" id="checkCaptcha">
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
$("#verificationCodeImg").click(function(){
$(this).hide().attr('src', '/captcha/getCaptcha?dt=' + new Date().getTime()).fadeIn();
});
$("#checkCaptcha").click(function () {
$.ajax({
type: "post",
url: "/captcha/check",
data: {
captcha: $("#inputCaptcha").val()
},
success: function(res) {
if(res) {
location.href = "success.html";
}else {
alert("验证码错误!!");
}
}
});
});
</script>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>验证成功页</title>
</head>
<body>
<h1>验证成功</h1>
</body>
</html>
END