HTML

参考

https://www.w3cschool.cn/html/html-form.html

基本语法

image-20241105214019480

基本结构

image-20241105214231412

不允许交叉嵌套

1
2
3
4
5
6
7
8
9
正确:
<div>
<a></a>
</div>
错误:
<div>
<a>
</div>
</a>

image-20241105214602794

1
2
3
4
<DOCTYPE html>
<!--这个头部是为了告诉浏览器解析HTML的标准
HTML分为HTML,xhtml,HTML5等不同版本
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>
test
</title>
</head>

<body>

</body>

</html>

vscode中输入!+tab可以自动补全

简单页面小代码

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试</title>
</head>
<body>
<h1>欢迎来到测试页面</h1>
<a href="https://space.bilibili.com/375081822/favlist?fid=3135803622&ftype=create ">好康的</a>
</body>
</html>

效果image-20241105220636597

image-20241105220543803

头部信息

image-20241105220737041

常用标签

image-20241106150120812

1
2
3
<b></b>
<strong></strong>
<!--区别在于strong标签可以优先被搜索引擎找到-->

image-20241106150914285

列表中也可以放链接

1
2
3
4
5
<ul>
<li><a href="https://space.bilibili.com/375081822/favlist?fid=3135803622&ftype=create">好康的</a></li>
<li><a href="https://www.baidu.com">好用的</a></li>
<li>ccc</li>
</ul>
image-20241106152130453

image-20241106152446484

锚点就像goto和标签,#作用是定位,比如./h1.html#chiikawa1意思就是跳转到h1.html下的chiikawa1锚点

image-20241106153736437

合并单元格:由左向右,由上到下

image-20241106154356090

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="./h1.html" method="GET"><!--用get方法将参数传到./h1.html-->
<input type="text" name="uname"><br/><br/><!--type表示表单类型,文本框,密码框,提交按钮-->
<input type="password" name="upass"><br/><br/>
<input type="submit" value="登录">
</form>
</body>
</html>

image-20241106160459005

image-20241106160622879

CSS

image-20241112204644589

语法

image-20241112210952820

选择器:选的是中的标签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--css样式位于style标签中-->
<!DOCTYPE html>
<html>
<head>
<meta charset="GBK">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
strong{font-size: 20;color: aquamarine;}
</style>
</head>
<body>
<strong>这是CSS测试页面</strong>
<ul>
<li>
<a href="https://www.bilibili.com">学习网站</a>
</li>
<li>CSS测试</li>
<li>油专</li>
</ul>
</body>
</html>

使用方法

image-20241112211552861

1.外部样式表:

image-20241112214706096

html中用link单标签引用

1
2
/*mycss.css*/
h1{font-size: 30;background-color: aquamarine;color: crimson;}

2.内嵌方法

嵌入在style中

3.内联样式

1
2
3
4
<!-- 直接将style写入标签中-->
<ul>
<li style="font-size:25;color:red"></li>
</ul>

CSS2常用选择器

image-20241113143203446

html选择器

把html的标签作为选择器,比如h1,a,也可以用*表示对所有标签生效(可能有兼容性问题)

缺点:过于笼统,html中有很多标签是重复的

类选择器

在标签中加入class属性,在创建css样式时用 (标签名).属性值进行标记

属性名可写可不写,不写属性名,那么只要class属性值等于css样式前的类属性值,css就会对该标签生效

image-20241113145110647

id选择器

根据标签中的id属性的属性值进行选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<meta charset="GBK">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#hid{font-size: 20;color: aquamarine;}/*#开始*/
</style>

</head>
<body>
<strong>这是CSS测试页面</strong>
<ul>
<li id="hid">
<a href="https://www.bilibili.com">学习网站</a>
</li>
<li>CSS测试</li>
<li>油专</li>
</ul>
</body>
</html>

image-20241113151502621

关联选择器/包含选择器

image-20241113151832026

组合选择器

image-20241113151857100

伪类选择器image-20241113152003635

常用属性

image-20241116141614334

尺寸与单位

image-20241116141922772

颜色

image-20241116142012471

image-20241116142144234

字体属性font

image-20241116142434890

image-20241116143211419

文本属性

image-20241116143406170

image-20241116143422896

JavaScript

image-20241119164618385

引入方式

image-20241119165144002

书写语法

image-20241119165641575

image-20241119165911317

变量

image-20241119170445990

image-20241119170644091

image-20241119170730015

数据类型&运算符

image-20241119170948064

image-20241119171037540

image-20241119171256783

image-20241119171419300

image-20241119171550102

image-20241119171612159

parseInt()会从第一个字符开始匹配,直到遇到第一个非数字停止转换;所以如果转换的内容第一个字符就不是数字,转换结果就是NaN

image-20241119172004670

Day02-06. JS-函数

image-20241119172350774

image-20241119172541604

js函数可以接收任意个数参数,有几个形参就接收前几个参数

Day02-07. JS-对象-Array数组

image-20241119172827435

image-20241119173017067

image-20241119173224884

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = [1,2,3,4];
arr[10] = "佐佐木淳平";
arr.forEach(function(e){//相当于匿名内部类
console.log(e);
})
//箭头函数简化(lambda表达式)
var arr = [1,2,3,4];
arr[10] = "佐佐木淳平";
arr.forEach((e)=>{
console.log(e);
})

//splice()参数1:起始索引 参数2:删除个数

Day02-08. JS-对象-String字符串

image-20241119174136829

image-20241119174205541

substring参数1:开始索引,参数2:结束索引;左闭右开

Day02-09. JS-对象-JSON

image-20241119174436326

函数简化写法:

1
2
3
函数名(){

}

image-20241119174651818

image-20241119174803855

key-value形式

key必须用双引号包围

image-20241119175056791

image-20241119175150122

1
2
3
4
5
var json_str = '{"name":"wwwtty","password":114514}';
var json_obj = JSON.parse(json_str);
var json_str = JSON.stringify(json_obj);
alert(json_obj.name);
alert(json_str);

Day02-10. JS-对象-BOM

image-20241119185453379

image-20241119193346757

1
2
3
4
5
6
var res = window.confirm();//确认为true,取消为false
if(res){
alert("确定");
}else{
alert("取消");
}

image-20241119194157829

1
2
alert(location.href);
location.href="https://www.bilibili.com";//跳转到该网址

Day02-11. JS-对象-DOM

image-20241119195159129

Day02-12. JS-对象-DOM案例

image-20241119195435377

image-20241119195552066

可以用.innerHTML=修改div标签内文本内容

Day02-13. JS-事件-事件绑定&常见事件

image-20241119201210647

image-20241119201342163

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试</title>
</head>
<body>
<form action="" method="get">
<input type="button" value="点我" id="hhh">
</form>
<script>
function on(){
alert("点我干嘛?");
}
document.getElementById("hhh").onclick=function(){
alert("干嘛点我");
}


</script>
</body>
</html>

image-20241119202455816

焦点:比如光标在输入框中闪烁

Day02-14. JS-事件-案例

Vue

Day02-15. Vue-概述

image-20241119203955051

image-20241119204210374

Day02-16. Vue-指令-v-bind&v-model&v-on

Day02-17. Vue-指令-v-if&v-show&v-for

Day02-18. Vue-指令-案例

Day02-19. Vue-生命周期

Ajax

nginx打包部署

1
2
#vue项目目录下
npm run build

打包好后会有一个dist文件夹

部署:将dist复制到nginx的/html中,启动nginx.exe 占用80端口(默认)

image-20250416204044806

image-20250416204440011

若端口被占用,在nginx.conf里更改

Maven

image-20241120164940486

image-20241120165923695

image-20241120173152507

image-20241120174537451

image-20241124202223071

image-20241124202556434

依赖配置

image-20241124202749916

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <!--先写这个,其他idea可以代码提示-->
<version>1.5.6</version>
</dependency>
</dependencies>

image-20241124203647719

依赖传递

image-20241124203856449

排除依赖image-20241124204124508

依赖范围

image-20241124205452618

生命周期

image-20241124211735777

image-20241124211752639

image-20241124211957736

image-20241124212032323

SpringBoot

image-20250417203457434

image-20241124214239548

image-20241124214652524

image-20250416205936107

image-20250416210249296

image-20250416210302032

@RestController

@RestController 通常用于创建 REST API,其中每个方法的返回值都是 HTTP 响应体的一部分。这使得开发人员可以专注于业务逻辑,而不必担心视图解析和模型数据的填充。

@RequestMapping()注解

1
在 Java 的 Spring 框架中,@RequestMapping注解用于将 HTTP 请求映射到控制器的处理方法上。这个注解可以用于类或方法上,用于定义请求的 URL 模式、HTTP 方法(如 GET、POST)、请求参数、头部信息等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo.controller;

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

//请求处理类
@RestController
public class HelloController {

//请求路径
@RequestMapping("/Hello")
public String Hello(){
return "Hello SpringBoot";
}
}

HTTP协议

image-20241125164413610

image-20241125165152198

请求数据格式

请求行,请求头,请求体

image-20241125165931263

请求头与请求体之间用空行隔开

image-20241125170028877

常见请求头:

image-20241125165719305

User-Agent用于浏览器兼容性处理

响应数据格式

相应行,响应头,响应体/响应正文

image-20241125170308311

状态码

image-20241125170408069

image-20241125170527857

image-20241125170725201

最常见:image-20241125170957970

常见响应头

image-20241125171113513

Web服务器-Tomcat

image-20250416211501889

基本使用

image-20241125174320885

image-20250416212251052

端口冲突解决:

image-20241125173307496

程序解析

image-20250416212750472

创建springboot项目时要关联start.spring.io,所以创建时需要联网

image-20250416212848621

image-20250416213051842

image-20250416213101326

起步依赖的版本依赖于父工程,版本依赖于父工程版本

image-20250416213644225

Spring Boot-Web的maven依赖中已经内嵌了tomcat

image-20241125174015658

image-20250416213330507

请求响应

请求

image-20250416214125055

HttpServletRequest称为请求对象

HttpServletResponse称为响应对象

image-20241204160223484

DispatcherServlet称为前端控制器/核心控制器

image-20241204160805640

postman工具

image-20241204161524017

简单参数image-20241204161631646

image-20241204165153743

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.demo.controller;

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

@RestController
public class RequestController {
@RequestMapping("/Args")
//请求的参数名与接收参数名必须一致,不然接收到的就是null
public String RequestController(String name,String passwd){
String response = name+':'+passwd;
System.out.println(response);
return response;
}
}

image-20250416215444075

image-20241204165500368

image-20241204165558132

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

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

@RestController
public class RequestController {
@RequestMapping("/Args")
//请求的参数名与接收参数名必须一致,不然接收到的就是null
public String RequestController(@RequestParam(name = "name",required = true)String name, @RequestParam(name = "passwd") String password){
String response = name+':'+password;
System.out.println(response);
return response;
}
}

image-20250416215756230

image-20250416215819660

image-20250416215852456

实体参数

image-20241204170401173

实体都放在pojo下(自己建目录)

image-20250416220501221

image-20250416220602362

至少要有请求参数

image-20250416220807637

1
2
3
4
5
@RequestMapping("/simplePojo")
public String simplyPojo(User user){
System.out.println(user);
return "OK";
}

image-20250417190259676

如果改变参数名字就不能封装进去,参数值为null

image-20250417190352654

image-20241204171709972

请求如下:

image-20250417190905367

image-20250417191007007

image-20250417191130946

image-20250417191330869

image-20250417191437056控制台输出

数组参数

image-20250416214712902

image-20250417191956083

1
2
3
4
5
@RequestMapping("/arrayParam")
public String arrayParam(String[] param){
System.out.println(Arrays.toString(param));
return "OK";
}

image-20250417192127800

集合参数

image-20250417192219602

不用@RequestParam会被解释成数组而非集合

image-20250417192522019

image-20250417192553966

日期参数

image-20250417193302287

image-20250417193712205

image-20250417193742315

json参数

image-20250417193843243

postman请求json参数方法:

image-20250417194252409

url中要加上请求路径,否则报错

image-20250417194953911

键名与形参对象属性名相同

image-20250417194516998

使用@RequestBody标识

1
2
3
4
5
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){
System.out.println(user);
return "OK";
}

image-20250417195009225

路径参数

单个路径

image-20250417195216812

image-20250417195556231

多个路径

image-20250417195836541

响应

@ResponseBody注解

image-20250417200126906

响应字符串

1
2
3
4
@RequestMapping("/str")
public String stringResponse() {
return "Hello SpringBoot";
}

image-20250417201221679

响应对象

响应格式为json

1
2
3
4
5
6
7
@RequestMapping("/obj")
public Address objResponse() {
Address address = new Address();
address.setProvince("福建");
address.setCity("泉州");
return address;
}

image-20250417201254954

响应集合

响应格式为json数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping("/List")
public List<Address> listResponse() {
Address address = new Address();
address.setProvince("福建");
address.setCity("泉州");
Address address2 = new Address();
address2.setProvince("福建");
address2.setCity("厦门");

List<Address> list = new ArrayList<>();
list.add(address2);
list.add(address);

return list;
}

image-20250417201350422

统一响应结果

上面三种响应格式都不同,不便于前后端开发

image-20250417201534271

image-20250417201653548

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
package com.example.demo.POJO;

public class Result {
//响应码 success:0 error:-1
private Integer code;
//提示信息
private String msg;
//返回数据
private Object data;


public Result() {
}

public Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}

@Override
public String toString() {
return "Result{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
public static Result success(Object data){
return new Result(0,"success",data);
}

public static Result success(){
return new Result(0,"success",null);
}

public static Result error(String errMsg){
return new Result(-1,errMsg,null);
}
}

static方法用于快速构建Result对象

响应字符串
1
2
3
4
5
@RequestMapping("/str")
public Result stringResponse() {
//return new Result(0,"success","Hello SpringBoot");
return Result.success("Hello SpringBoot");
}

image-20250417202949112

响应对象

响应格式为json

1
2
3
4
5
6
7
8
@RequestMapping("/obj")
public Result objResponse() {
Address address = new Address();
address.setProvince("福建");
address.setCity("泉州");
//return new Result(0,"success",address);
return Result.success(address);
}

image-20250417203015137

响应集合

响应格式为json数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping("/List")
public Result listResponse() {
Address address = new Address();
address.setProvince("福建");
address.setCity("泉州");
Address address2 = new Address();
address2.setProvince("福建");
address2.setCity("厦门");

List<Address> list = new ArrayList<>();
list.add(address2);
list.add(address);

//return new Result(0,"success",list);
return Result.success(list);
}

image-20250417203041487

案例

image-20250417204553667

pom.xml中添加坐标

1
2
3
4
5
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>

工具类XmlParserUtils

image-20250417204611577

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

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;

/**
* XML解析工具类
* 提供将XML文件解析为Java对象列表的功能
*/
public class XmlParserUtils {

/**
* 将XML文件解析为指定类型的对象列表
*
* @param file XML文件路径
* @param targetClass 目标对象类型
* @param <T> 泛型类型
* @return 解析后的对象列表
* @throws DocumentException 如果XML解析失败
*/
public static <T> List<T> parse(String file, Class<T> targetClass) throws DocumentException {
// 1. 创建SAXReader对象用于读取XML
SAXReader reader = new SAXReader();

// 2. 读取XML文件并获取Document对象
Document document = reader.read(new File(file));

// 3. 获取XML根元素
Element rootElement = document.getRootElement();

// 4. 获取所有emp元素
List<Element> elements = rootElement.elements("emp");

// 5. 准备返回的结果列表
List<T> list = new ArrayList<>();

try {
// 6. 遍历集合,得到每一个emp标签
for (Element element : elements) {
// 获取name属性
String name = element.element("name").getText();
// 获取age属性
String age = element.element("age").getText();
// 获取image属性
String image = element.element("image").getText();
// 获取gender属性
String gender = element.element("gender").getText();
// 获取job属性
String job = element.element("job").getText();

// 7. 获取目标类的构造方法
Constructor<T> constructor = targetClass.getDeclaredConstructor(
String.class, Integer.class, String.class, String.class, String.class
);

// 8. 设置构造方法可访问(即使是私有构造方法)
constructor.setAccessible(true);

// 9. 使用反射创建对象实例
T object = constructor.newInstance(
name,
Integer.parseInt(age),
image,
gender,
job
);

// 10. 将创建的对象添加到列表
list.add(object);
}
} catch (Exception e) {
throw new RuntimeException("XML解析为对象失败", e);
}

return list;
}
}

Emp类

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
public class Emp {
private String name;
private Integer age;
private String image;
private String gender;
private String job;

public Emp() {
}

public Emp(String name, Integer age, String image, String gender, String job) {
this.name = name;
this.age = age;
this.image = image;
this.gender = gender;
this.job = job;
}

public String getName() {
return name;
}

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

public Integer getAge() {
return age;
}

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

public String getImage() {
return image;
}

public void setImage(String image) {
this.image = image;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public String getJob() {
return job;
}

public void setJob(String job) {
this.job = job;
}
}

处理请求类EmpController

假设前端请求路径如下:

image-20250417212515559

那么@RequestMapping中的路径就为”/listEmp”

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

import com.example.demo.POJO.Emp;
import com.example.demo.POJO.Result;
import com.example.demo.Utils.XmlParserUtils;
import org.dom4j.DocumentException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class EmpController {
@RequestMapping("/listEmp")
public Result list() throws DocumentException {
//1.加载并解析emp.xml
//动态加载xml文件
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
//2.对数据进行转换处理
//流式处理
empList.stream().forEach(emp -> {
String gender = emp.getGender();
if ("1".equals(gender)) {
emp.setGender("男");
} else if ("2".equals(gender)) {
emp.setGender("女");
}
String job = emp.getJob();
if ("1".equals(job)) {
emp.setJob("老师");
} else if ("2".equals(job)) {
emp.setJob("主任");
}else if("3".equals(job)){
emp.setJob("就业指导");
}
});
//3.响应数据
return Result.success(empList);
}
}

响应结果:

image-20250417213927717

分层解耦

三层架构

image-20250417210400405

image-20250417210427254

image-20250417210517928

Dao层-数据访问

Dao对数据的访问方式很多(文件,数据库等),想要灵活处理需要使用接口

image-20250417214158607

1
2
3
4
5
6
7
8
9
10
package com.example.demo.Dao;

import com.example.demo.POJO.Emp;

import java.util.List;

public interface EmpDao {
//获取员工列表
public List<Emp> getListEmp();
}

它的实现类A:

image-20250417214439110

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.demo.Dao.impl;

import com.example.demo.Dao.EmpDao;
import com.example.demo.POJO.Emp;
import com.example.demo.Utils.XmlParserUtils;
import org.dom4j.DocumentException;

import java.util.List;

public class EmpDaoA implements EmpDao {
@Override
public List<Emp> getListEmp() throws DocumentException {
//1.加载并解析emp.xml
//动态加载xml文件
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
return XmlParserUtils.parse(file, Emp.class);
}
}

Service层-业务逻辑处理

与Dao层类似,想要灵活处理,使用接口

image-20250417214923497

1
2
3
4
5
6
7
8
9
10
package com.example.demo.service;

import com.example.demo.POJO.Emp;

import java.util.List;

public interface EmpService {
//返回处理后的emp列表
public List<Emp> ListEmp();
}

它的实现类:

要处理数据,需要从Dao层获取,那么需要定义Dao对象

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
package com.example.demo.service.impl;

import com.example.demo.Dao.EmpDao;
import com.example.demo.Dao.impl.EmpDaoA;
import com.example.demo.POJO.Emp;
import com.example.demo.service.EmpService;
import org.dom4j.DocumentException;

import java.util.List;

public class EmpServiceA implements EmpService {
//面向接口编程
private EmpDao empDao = new EmpDaoA();

@Override
public List<Emp> ListEmp() throws DocumentException {
List<Emp> empList = empDao.getListEmp();
empList.stream().forEach(emp -> {
String gender = emp.getGender();
if ("1".equals(gender)) {
emp.setGender("男");
} else if ("2".equals(gender)) {
emp.setGender("女");
}
String job = emp.getJob();
if ("1".equals(job)) {
emp.setJob("老师");
} else if ("2".equals(job)) {
emp.setJob("主任");
} else if ("3".equals(job)) {
emp.setJob("就业指导");
}
});

return empList;
}
}

Controller层-接收请求,响应数据

需要调底下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
package com.example.demo.controller;

import com.example.demo.Dao.EmpDao;
import com.example.demo.POJO.Emp;
import com.example.demo.POJO.Result;
import com.example.demo.Utils.XmlParserUtils;
import com.example.demo.service.EmpService;
import com.example.demo.service.impl.EmpServiceA;
import org.dom4j.DocumentException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class EmpController {
private EmpService empService = new EmpServiceA();
@RequestMapping("/listEmp")
public Result list() throws DocumentException {
//1.加载并解析emp.xml
//2.对数据进行转换处理
List<Emp> empList = empService.ListEmp();
//3.响应数据
return Result.success(empList);
}
}

image-20250417215752235

分层解耦

image-20250417220049558

EmpController中的empService是Service层实现类的实例,说明这两层耦合

解决方法:容器

image-20250417220320616

image-20250417220343493

image-20250417220457909

步骤

image-20250418201847743

@Component注解

在Dao层、Service实现类前加上该注解,表示将该类交给IOC容器管理,成为IOC容器中的bean

这样一来Controller层想要切换底层,直接更改@Component作用的类就行

1
2
3
4
5
6
7
8
9
10
@Component
public class EmpDaoA implements EmpDao {
@Override
public List<Emp> getListEmp() throws DocumentException {
//1.加载并解析emp.xml
//动态加载xml文件
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
return XmlParserUtils.parse(file, Emp.class);
}
}

@Autowired注解

运行时,IOC容器会提供该类型的bean对象,并赋值给该变量 - 依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class EmpController {
@Autowired
private EmpService empService;
@RequestMapping("/listEmp")
public Result list() throws DocumentException {
//1.加载并解析emp.xml
//2.对数据进行转换处理
List<Emp> empList = empService.ListEmp();
//3.响应数据
return Result.success(empList);
}
}

IOC详解

Bean的声明

image-20250418203528345

Controller层不用再加@Controller,因为@RestController=@Repository+@Controller

image-20250418203737224

为什么称之为@Component的衍生类?

image-20250418204430796

image-20250418204407308

1
2
//可以以这样的形式为Bean对象起名字
@Service("Name")

注意事项

image-20250418204546486

Bean组件扫描

image-20250418204842261

规范:将Dao放在启动类所在包

不规范:找不到包

image-20250418205159062

就需要在启动类上使用@ComponentScan

image-20250418205005662

源码显示,@ComponentScan需要接受String数组作为所在包

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

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan({"Dao","com.example.demo"})
@SpringBootApplication
public class DemoApplication {

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

}

我们为什么数组中传入两个包呢?

@SpringBootApplication中集成的@ComponentScan扫描的是启动类所在的包;

在@ComponentScan中声明其他的包会覆盖掉启动类所在的包,所以需要重新声明启动类所在的包

DI详解

如果有多个相同类型的Bean:

image-20250418211728937

image-20250418211743623

image-20250418211703044

image-20250418211232732

@Primary

多个相同类型的Bean,加上@Primary的那个生效

image-20250418211916896

image-20250418211907939

image-20250418212004613

image-20250418211955691

@Qualifier

在@AutoWired前加上@Qualifier(“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
package com.example.demo.controller;

import com.example.demo.POJO.Emp;
import com.example.demo.POJO.Result;
import com.example.demo.service.EmpService;
import org.dom4j.DocumentException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class EmpController {
//EmpServiceA默认的bean名字
@Qualifier("empServiceA")
@Autowired
private EmpService empService;
@RequestMapping("/listEmp")
public Result list() throws DocumentException {
//1.加载并解析emp.xml
//2.对数据进行转换处理
List<Emp> empList = empService.ListEmp();
//3.响应数据
return Result.success(empList);
}
}

@Resource

与@Qualifier的区别:

  • @Qualifier按照类型注入

  • @Resource按照名称注入;使用@Resource后就不用@Autowired了

    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
    package com.example.demo.controller;

    import com.example.demo.POJO.Emp;
    import com.example.demo.POJO.Result;
    import com.example.demo.service.EmpService;
    import jakarta.annotation.Resource;
    import org.dom4j.DocumentException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.List;

    @RestController
    public class EmpController {
    // @Qualifier("empServiceA")
    // @Autowired
    @Resource(name = "empServiceB")
    private EmpService empService;
    @RequestMapping("/listEmp")
    public Result list() throws DocumentException {
    //1.加载并解析emp.xml
    //2.对数据进行转换处理
    List<Emp> empList = empService.ListEmp();
    //3.响应数据
    return Result.success(empList);
    }
    }
image-20250418212814266

Mybatis

image-20250418213624926

准备工作

image-20250418213948765

image-20250420162905928

image-20250420163058104

image-20250420163042923

springboot工程

image-20250418214309975

image-20250418214350129

配置文件

1
2
3
4
5
6
7
8
spring.application.name=Mybatis-demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库名
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#用户名
spring.datasource.username=root
#密码
spring.datasource.password=114514

@Mapper注解

Mybatis中Mapper层其实就和Dao层差不多

在运行时,会自动生成该接口的实现类对象(代理对象),并且将该对象交给IOC容器管理

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.wwwtty.mybatisdemo.mapper;

import com.wwwtty.mybatisdemo.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface UserMapper {
@Select("select* from user")
public List<User> list();
}

进行单元测试

image-20250420163551135

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

import com.wwwtty.mybatisdemo.mapper.UserMapper;
import com.wwwtty.mybatisdemo.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class MybatisDemoApplicationTests {

@Autowired
UserMapper userMapper;

@Test
public void UserListTest(){
List<User> list = userMapper.list();
list.stream().forEach(user ->{
System.out.println(user);
});
}
}

@SpringBootTest进行单元测试时,也会加载整个springboot环境

image-20250420170435640

配置sql提示

image-20250420201158998

image-20250420201430768

image-20250420201500926

image-20250420201524960

image-20250420201822363

image-20250420202059036

功能强大qaq:

image-20250420202159898

JDBC

(面向接口编程)

JDBC只提供接口,由数据库厂商实现具体方法

image-20250420202340362

缺点:

image-20250420202552672

image-20250420202708569

数据库连接池

image-20250420202838234

image-20250420202924564

image-20250420203009321

image-20250420203204017

image-20250420203059719

实现了DataSource接口

image-20250420203123699

切换连接池

只需要引入maven依赖

image-20250420203258391

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>

本地测试还得在properties文件加上:

1
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

才能:

image-20250420204600465

或者:

由于我的springboot版本为:

image-20250420205002180

因此坐标中的druid-spring-boot-starter应该改成druid-spring-boot-3-starter

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.22</version>
</dependency>

image-20250420204922741

另一种配置方式

image-20250420205145225

lombok

pojo类算上getter/setter等方法之后,太过于臃肿

image-20250420205336460

image-20250420205419808

引入依赖

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

@Data

包含了@Getter,@Setter,@ToString,@EqualsAndHashCode

不包含构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.wwwtty.mybatisdemo.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String email;
}

image-20250420210030897

上述注解的作用在于根据注解生成一系列方法

User的字节码文件反编译:

image-20250420210155253

image-20250420210230204

Mybatis基础操作

删除操作

@Delete

1
2
3
4
5
6
7
8
9
10
package com.wwwtty.basic_op.Mapper;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface EmpMapper {
@Delete("delete from emp where id = 1")
void delete();
}

这么做的局限性是,id是静态的,但是前端传进来的id参数肯定是动态的

传递参数

在@Delete中将参数用#{}包围

image-20250426152414630

1
2
3
4
5
6
7
8
9
10
package com.wwwtty.basic_op.Mapper;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface EmpMapper {
@Delete("delete from emp where id = #{id}")
void delete(Integer id);
}

如何拿到影响的记录数呢,改一下返回值就可以

1
2
3
4
5
6
7
8
9
10
package com.wwwtty.basic_op.Mapper;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface EmpMapper {
@Delete("delete from emp where id = 1")
Integer delete();
}
1
2
3
4
@Test
void contextLoads() {
System.out.println(empMapper.delete(5));
}

image-20250426152241559

测试

表内容如下:

image-20250426151816452

用@Autowired注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.wwwtty.basic_op;

import com.wwwtty.basic_op.Mapper.EmpMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class BasicOpApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
void contextLoads() {
empMapper.delete(5);
}
}

image-20250426151830288

Mybatis日志输出

1
2
#mybatis日志输出,输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

运行结果:image-20250426152754662

使用了参数化查询技术(预编译查询)

预编译SQL优势

image-20250426153102976

参数占位符

image-20250426153355879

新增操作

@Insert

insert中的字段太多,可以直接封装成一个对象

用lombok封装getter/setter等方法

image-20250426154316266

1
2
3
4
5
6
7
8
9
10
11
package com.wwwtty.mybatis_insert.Mapper;

import com.wwwtty.mybatis_insert.pojo.Emp;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface EmpMapper {
@Insert("insert into emp (id,name,email) values (#{id},#{name},#{email})")
void Insert(Emp emp);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.wwwtty.mybatis_insert;

import com.wwwtty.mybatis_insert.Mapper.EmpMapper;
import com.wwwtty.mybatis_insert.pojo.Emp;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class MybatisInsertApplicationTests {
@Autowired
EmpMapper empMapper;
@Test
void contextLoads() {
Emp emp = new Emp();
emp.setId(5);
emp.setEmail("114514@acceed.com");
emp.setName("淳平");

empMapper.Insert(emp);
}
}

日志输出:

image-20250426155110184

主键返回

image-20250426155301757

@Options

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

import com.wwwtty.mybatis_insert.pojo.Emp;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;

@Mapper
public interface EmpMapper {

/*
* keyProperty:主键值赋值给实体类哪个对象
* useGeneratedKeys:获取主键值
* */
@Options(keyProperty = "id", useGeneratedKeys = true)
@Insert("insert into emp (name,email) values (#{name},#{email})")
void Insert(Emp emp);
}

这样一来getId()就不会返回null

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

import com.wwwtty.mybatis_insert.Mapper.EmpMapper;
import com.wwwtty.mybatis_insert.pojo.Emp;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class MybatisInsertApplicationTests {
@Autowired
EmpMapper empMapper;
@Test
void contextLoads() {
Emp emp = new Emp();
// emp.setId(5);
emp.setEmail("114514@acceed.com");
emp.setName("林丹");

empMapper.Insert(emp);
System.out.println(emp.getId());
}
}

image-20250426161006342

更新操作

@Update

image-20250426161909725

1
2
3
4
5
6
7
8
9
10
11
package com.wwwtty.mybatis_update.Mapper;

import com.wwwtty.mybatis_update.pojo.Emp;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface EmpMapper {
@Update("update emp set name=#{name},email=#{email} where id=#{id}")
void update(Emp emp);
}

执行结果:

image-20250426162430561

image-20250426162436087

查询操作

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.wwwtty.mybatis_select.Mapper;

import com.wwwtty.mybatis_select.pojo.Emp;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface EmpMapper {
@Select("select* from emp where id = #{id}")
List<Emp> getById(Integer id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.wwwtty.mybatis_select;

import com.wwwtty.mybatis_select.Mapper.EmpMapper;
import com.wwwtty.mybatis_select.pojo.Emp;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class MybatisSelectApplicationTests {
@Autowired
EmpMapper empMapper;
@Test
void contextLoads() {
Emp emp = new Emp();
emp.setId(5);
System.out.println(empMapper.getById(5));
}
}

运行结果:

image-20250426164017496

数据封装

image-20250426164121784

image-20250426164220088

@Results,@Result

1
2
3
4
5
6
7
8
/*
* column:字段名
* property:类中属性名
* */
@Results({
@Result(column = "",property = ""),
@Result(column = "",property = "")
})

开启mybatis驼峰命名自动映射开关 a_column -> aColumn

1
2
#开启mybatis驼峰命名自动映射开关(直接搜索骆驼(camel))
mybatis.configuration.map-underscore-to-camel-case=true

条件查询

image-20250426165935370

image-20250426165951149

1
select * from user where name like '%张%' and gender=1 and entrydate between '2010-01-01' and '2020-01-01' order by update_time desc

这些参数不好封装到一个对象中去,直接传参数

1
2
3
@Select("select * from user where name like '%${name}%' and gender=#{gender} and " +
"entrydate between #{begin} and #{end} order by update_time desc")
List<User> list(String name, Short gender, LocalDate begin,LocalDate end);

进行模糊匹配的时候,要用’%%’的形式,这样就不能用预编译的#{}(占位符不能出现在引号内),需要用拼接sql的${}(不推荐)

1
2
3
4
5
6
7
@Test
void test() {
List<User> res = empMapper.list("张", (short) 1, LocalDate.of(2010, 1, 1), LocalDate.of(2020, 1, 1));
res.stream().forEach(user -> {
System.out.println(user);
});
}

image-20250426171629304

怎么解决拼接的问题

concat()

1
2
3
@Select("select * from user where name like concat('%',#{name},'%') and gender=#{gender} and " +
"entrydate between #{begin} and #{end} order by update_time desc")
List<User> list(String name, Short gender, LocalDate begin,LocalDate end);

image-20250426172027177

早期版本不会保留形参名

image-20250426172059490

XML映射文件

image-20250428203330256

image-20250428203708238

与包名一致

image-20250428204102085

与接口名一致

image-20250428204545697

xml文件的约束:https://mybatis.p2hp.com/getting-started.html

1
2
3
4
5
6
7
8
9
10
11
12
<?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根标签-->
<mapper namespace="com.wwwtty.xml_reflect.mapper.EmpMapper">
<!-- id:Mapper类中的方法名 -->
<!-- resultType:返回值所封装的单条记录的类型的全类名,比如返回值是List<User>,填的就是User的全类名 -->
<select id="list" resultType="">

</select>
</mapper>

获取全类名:

image-20250428204919955

原查询语句:

1
2
3
@Select("select * from user where name like concat('%',#{name},'%') and gender=#{gender} and " +
"entrydate between #{begin} and #{end} order by update_time desc")
List<User> list(String name, Short gender, LocalDate begin,LocalDate end);

改用xml映射的方式:

如果xml中sql语句没有高亮,上下文操作中选择注入语言

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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根标签-->
<mapper namespace="com.wwwtty.xml_reflect.mapper.EmpMapper">
<!-- id:Mapper类中的方法名 -->
<!-- resultType:返回值所封装的单条记录的类型的全类名,比如返回值是List<User>,填的就是User的全类名 -->
<select id="list" resultType="com.wwwtty.xml_reflect.pojo.User">
select * from user where name like concat('%',#{name},'%') and gender=#{gender} and
entrydate between #{begin} and #{end} order by update_time desc
</select>
</mapper>

xml映射适合于复杂sql

动态sql

image-20250428210744924

< if >

image-20250428211702486

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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根标签-->
<mapper namespace="com.wwwtty.xml_reflect.mapper.EmpMapper">
<!-- id:Mapper类中的方法名 -->
<!-- resultType:返回值所封装的单条记录的类型的全类名,比如返回值是List<User>,填的就是User的全类名 -->
<select id="list" resultType="com.wwwtty.xml_reflect.pojo.User">
select *
from user
where
<if test="name != null">
name like concat('%', #{name}, '%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
order by update_time desc
</select>
</mapper>
1
empMapper.list(name,null,null,end);

相应的sql语句:

image-20250428212313281

< where >

在上面的例子中,如果我们只筛选性别:

image-20250428212633326

但是这个and还很不好删除(容易别的地方出错)

作用:根据if的结果(子元素),自动添加/删除where;同时自动删除多余的and和or

使用< where >解决

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" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--mapper根标签-->
<mapper namespace="com.wwwtty.xml_reflect.mapper.EmpMapper">
<!-- id:Mapper类中的方法名 -->
<!-- resultType:返回值所封装的单条记录的类型的全类名,比如返回值是List<User>,填的就是User的全类名 -->
<select id="list" resultType="com.wwwtty.xml_reflect.pojo.User">
select *
from user
<where>
<if test="name != null">
name like concat('%', #{name}, '%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
order by update_time desc
</where>
</select>
</mapper>

运行生成的sql:

image-20250428213048653

< set >

去除set中多余的逗号

案例:动态更新

如果我们这样写动态更新的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
<update id="update">
update user
set
<if test="username != null">
username=#{username},
</if>
<if test="password != null">
password=#{password},
</if>
<if test="name != null">
name=#{name},
</if>
<if test="gender != null">
gender=#{gender},
</if>
<if test="image != null">
image=#{image},
</if>
<if test="job != null">
job=#{job},
</if>
<if test="entrydate != null">
entrydate=#{entrydate},
</if>
<if test="deptId != null">
dept_id=#{deptId},
</if>
<if test="createDate != null">
create_time=#{createTime},
</if>

<if test="updateTime != null">
update_time=#{updateTime}
</if>
where id = #{id}
</update>

image-20250428215649758

会有多一个逗号的情况

使用 < set >解决

image-20250428215812928

< foreach >

场景:批量删除

1
delete from user where id in(3,4,5);
1
void deleteByIds(List<Integer> list);
1
2
3
4
5
<delete id="deleteByIds">
<foreach collection="" item="" separator="" open="" close="">

</foreach>
</delete>
  • collection:要遍历的集合

  • item:遍历出的元素

  • separator:分隔符

  • open:遍历开始前拼接的sql片段

  • close:遍历结束后拼接的sql片段

    1
    2
    3
    4
    5
    6
    7
    <!--    delete from user where id in(3,4,5)    -->
    <delete id="deleteByIds">
    delete from user where id in
    <foreach collection="list" item="id" separator="," open="(" close=")">
    #{id}
    </foreach>
    </delete>

< sql >

< sql >用于封装一些重复使用的sql片段,并分配一个唯一的id

< include >

用< include >包含< sql >封装的sql片段,用refid=id从而完成包含

1
2
3
void selectArgs();

void selectById();
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
<sql id="basicSelect">
select username,
password,
name,
gender,
id,
username,
password,
name,
gender,
image,
job,
entrydate,
dept_id,
create_time,
update_time
from user
</sql>

<select id="selectArgs">
<include refid="basicSelect" />
</select>
<select id="selectById">
<include refid="basicSelect" />

</select>

image-20250429211401588

文件上传

前端表单:

1
2
3
4
5
6
<form action="" method="post" enctype="multipart/form-data">
<input type="text" name="name">
<input type="password" name="passwd">
<input type="file" name="file">
<button type="submit">submit</button>
</form>

image-20250527213914701

若不指定enctype=”multipart/form-data”,上传时只会上传文件名

image-20250527214515050

image-20250527214520805

改成enctype=”multipart/form-data”

image-20250527214546829

请求中有了文件内容

image-20250527214629513

image-20250527214745685

后端:

1
2
3
4
5
6
7
8
9
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String name,String passwd,@RequestParam("file") MultipartFile image){
log.info("用户名:{},密码:{},收到文件:{}",name,passwd,image);
return Result.success();
}
}

进行测试:

image-20250527220342050

本地存储

服务端,接收到上传的文件后,存储在本地服务器磁盘

demo:

在static目录下新建了一个upload目录

image-20250528154900532

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String name,String passwd,@RequestParam("file") MultipartFile image) throws Exception {
log.info("用户名:{},密码:{},收到文件:{}",name,passwd,image);
//获取原始文件名
String fileName = image.getOriginalFilename();

//本地存储
image.transferTo(new File("static/upload"+fileName));

return Result.success();
}
}
postman测试

image-20250528155150879

image-20250528155649268

image-20250528155636701

存在问题:

如果两个用户上传了相同名字的文件,那么就覆盖了

所以我们需要构造唯一的文件名

使用uuid
1
2
3
4
5
@Test
void contextLoads() {
String uuid = UUID.randomUUID().toString();
System.out.println(uuid);
}
简单的解决:(这么做有漏洞)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@PostMapping("/upload")
public Result upload(String name,String passwd,@RequestParam("file") MultipartFile image) throws Exception {
log.info("用户名:{},密码:{},收到文件:{}",name,passwd,image);
//获取原始文件名
String originalFilename = image.getOriginalFilename();

//最后一个.的位置
int index = originalFilename.lastIndexOf('.');

//获取拓展名
String extName = originalFilename.substring(index);

//拼接文件名
String fileName = UUID.randomUUID().toString()+extName;

//本地存储
image.transferTo(new File("D:\\JavaWeb\\Spring-Mybatis-demo\\src\\main\\resources\\static\\upload\\"+fileName));

return Result.success();
}

如果上传一个大文件

image-20250528161104341

这是因为

image-20250528161122796

application.properties中

1
2
3
4
#单个文件限制大小
spring.servlet.multipart.max-file-size=10MB
#单个请求上传大小
spring.servlet.multipart.max-request-size=100MB

image-20250528161605915

云存储

image-20250528161927187

官方文档

文件上传SDK

复制官方代码

image-20250528170128371

获取endpoint

image-20250528170345696

设置上传到OSS中的文件名以及要上传的文件路径
1
2
3
4
5
6
// 在OSS中叫什么名字
String objectName = "test.md";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "D:\\My_Blogs\\myblogs\\source\\_posts\\Vue.md";
// 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。

image-20250528171849653

bucket文件列表中查看

image-20250528171915167

集成Aliyun-OSS

接口文档

image-20250529134903683

image-20250529134922220

image-20250529134944310

工具类AliOSSUtils

官方代码改写

没有写成静态方法,就用IOC容器注入;工具类不属于三层架构,直接使用@Component注解

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
@Slf4j
@Component
public class AliOSSUtils {

// 获取上传的文件的输入流
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-chengdu.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
String bucketName = "k4n9l4n";
String region = "cn-chengdu";

/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws Exception {
String oringinalFileName = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString()+oringinalFileName.substring(oringinalFileName.lastIndexOf("."));

EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 创建OSSClient实例。
// 当OSSClient实例不再使用时,调用shutdown方法以释放资源。
ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialsProvider)
.clientConfiguration(clientBuilderConfiguration)
.region(region)
.build();

try {
InputStream inputStream = file.getInputStream();
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream);
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);

String url = endpoint.split("//")[0]+"//"+bucketName+"."+endpoint.split("//")[1]+"/"+fileName;
return url;
} catch (OSSException oe) {
log.info("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
log.info("Error Message:" + oe.getErrorMessage());
log.info("Error Code:" + oe.getErrorCode());
log.info("Request ID:" + oe.getRequestId());
log.info("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
log.info("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
log.info("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return null;
}
}
Controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
@RestController
@RequestMapping("/upload")
public class UploadController {
@Autowired
private AliOSSUtils aliOSSUtils;

@PostMapping
public Result upload(MultipartFile image) throws Exception {
log.info("接受到文件:{}",image.getOriginalFilename());

String url = aliOSSUtils.upload(image);

if(url != null){
log.info("上传文件的url:{}",url);
return Result.success(url);
}
return Result.error("上传出错");
}
}
测试

image-20250529141610713

image-20250529142949932

案例

image-20250501194904854

开发规范

image-20250501200003215

Restful

image-20250501200305519

image-20250501200336571

统一响应结果

image-20250501200425157

开发流程

image-20250501200541756

部门管理

部门管理-查询

接口文档:

image-20250502104701029

image-20250502104743935

1
2
3
4
5
6
7
8
9
10
//使用该注解就不用自己new日志对象
@Slf4j
@RestController
public class deptController {
@RequestMapping("/dept")
public Result list(){
log.info("查询所有部门信息");
return Result.Success();
}
}

ps:构建项目时不要选lombok,否则要在pom.xml中注释以下内容:

image-20250502110941701

否则报错:

image-20250502110956067

postman测试:

image-20250502111138681

日志输出:

image-20250502111156255

指定请求方法
1
2
3
@RequestMapping(value = "/depts",method = RequestMethod.GET)
//简化版本
@GetMapping("depts")
注入依赖

Controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@RestController
public class deptController {
@Autowired
private deptService service;
@GetMapping("depts")
public Result list() {
log.info("查询所有部门信息");
//查询所有部门信息
List<Dept> deptList = service.list();
return Result.success(deptList);
}
}

Service层

1
2
3
4
5
6
7
8
9
10
@Service
public class deptImpl implements deptService {
@Autowired
private deptMapper mapper;

@Override
public List<Dept> list() {
return mapper.list();
}
}

Mapper层

1
2
3
4
5
@Mapper
public interface deptMapper {
@Select("select * from dept")
public List<Dept> list();
}

image-20250502112504837

部门管理-删除

接口文档

image-20250502113702239

流程

image-20250502113924420

Controller层
1
2
3
4
5
6
@DeleteMapping("/depts/{id}")
public Result delete(@PathVariable Integer id){
log.info("根据id删除部门:{}",id);
service.delete(id);
return Result.success();
}
Service层
1
2
3
4
@Override
public void delete(Integer id) {
mapper.deleteById(id);
}
Mapper层
1
2
@Delete("delete from dept where id=#{id}")
void deleteById(Integer id);
postman测试

image-20250502114723744

执行结果

image-20250502114642751

image-20250502114646465

部门管理-新增

接口文档

image-20250502115239802

image-20250502115559884

Controller层
1
2
3
4
5
6
@PostMapping("/depts")
public Result add(@RequestBody Dept dept){
log.info("新增部门{}",dept);
service.add(dept);
return Result.success();
}
Service层
1
2
3
4
5
6
7
@Override
public void add(Dept dept){
dept.setCreateTime(LocalDate.now());
dept.setUpdateTime(LocalDate.now());

mapper.insert(dept);
}
Mapper层
1
2
@Insert("insert into dept (name,create_time,update_time) values (#{name},#{createTime},#{updateTime})")
void insert(Dept dept);
postman测试

image-20250502120724193

结果

image-20250502120905590

image-20250502120921945

简化RequestMapping

image-20250502121354174

image-20250502121723903

员工管理

分页查询

分析

image-20250513210042683

接口文档

image-20250513210239794

image-20250513210246107

image-20250513210255167

image-20250513210324909

image-20250513210538282

封装实体类

image-20250513210839682

1
2
3
4
5
6
7
@Data
@NoArgsConstructor
@AllArgsConstructor
public class pageBean {
private Long total;
private List rows;
}
实现
Mapper层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Mapper
public interface EmpMapper {
/**
* 查询总记录数
* @return
*/
@Select("select count(*) from emp")
Long count();

/**
*
* @param page:起始页
* @param pageSize:查多少页
* @return
*/
@Select("select * from emp limit #{page},#{pageSize}")
List<Emp> page(Integer page,Integer pageSize);
}
Service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class empImpl implements empService {
@Autowired
EmpMapper empMapper;

@Override
public PageBean page(Integer page, Integer pageSize) {
Long total = empMapper.count();
Integer start = (page - 1) * pageSize;
List<Emp> rows = empMapper.page(start, pageSize);
//封装pageBean
PageBean pageBean = new PageBean(total,rows);
return pageBean;
}
}
Controller层

image-20250513211738018

用@RequestParam(defaultValue=””)设置参数默认值

1
2
3
4
5
6
7
@RestController
public class empController {
@GetMapping("/emps")
public Result page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize){
return new Result();
}
}

日志输出

1
log.info("分页查询,分页查询页码为{},每页记录数为{}",page,pageSize);

调用Service层

1
PageBean pageBean = empservice.page(page,pageSize);
1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@RestController
public class empController {
@Autowired
empService empservice;
@GetMapping("/emps")
public Result page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize){
log.info("分页查询,起始{},查询{}页",page,pageSize);
// 调用service层
PageBean pageBean = empservice.page(page,pageSize);
return Result.success(pageBean);
}
}
测试

postman

image-20250513215109589

前后端联调

image-20250513215305299

PageHelper插件

image-20250524165534782

引入依赖

这里的版本不能按照课件的1.4.2,否则会强转异常(ClassCastException)

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>

EmpMapper

1
2
@Select("select* from emp")
List<Emp> list();

EmpServer的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public PageBean page(Integer page, Integer pageSize) {
//设置分页参数
PageHelper.startPage(page,pageSize);

//执行查询
List<Emp> empList = empMapper.list();
Page<Emp> page1 = (Page<Emp>)empList;

//封装bean对象
PageBean pageBean = new PageBean(page1.getTotal(),page1.getResult());
return pageBean;
}

条件分页查询

接口文档

image-20250527201357121

Controller
1
2
3
4
5
6
7
8
//方法声明
public Result page(
@RequestParam(required = false) String name,
@RequestParam(required = false) Short gender,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end, @RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize
)
Service层
1
2
3
4
5
6
7
8
9
10
@Override
public PageBean page(String name, Short gender, LocalDate begin, LocalDate end, Integer page, Integer pageSize) {
PageHelper.startPage(page,pageSize);

List<Emp> empList = empMapper.page(name, gender, begin, end);
Page<Emp> empPage = (Page<Emp>) empList;
PageBean pageBean = new PageBean(empPage.getTotal(), empPage.getResult());

return pageBean;
}
Mapper层
1
2
3
4
5
6
7
8
/**
* @param name
* @param gender
* @param begin 起始create_time
* @param end 中止create_time
* @return
*/
List<Emp> page(String name, Short gender, LocalDate begin, LocalDate end);

使用了PageHelper之后,Mapper中就不要再使用page与pageSize参数,否则会导致生成的sql变为

1
SELECT count(0) FROM emp LIMIT ?, ?

就查不到了

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
<?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根标签-->
<mapper namespace="com.wwwtty.springmybatisdemo.Mapper.EmpMapper">
<!-- id:Mapper类中的方法名 -->
<!-- resultType:返回值所封装的单条记录的类型的全类名,比如返回值是List<User>,填的就是User的全类名 -->
<select id="page" resultType="com.wwwtty.springmybatisdemo.pojo.Emp">
select * from emp
<where>
<!-- 这里还需要判断空字符串,否则会多拼上name like %% -->
<if test="name != null and name != ''">
name like concat('%',#{name},'%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
</where>
</select>
</mapper>

删除员工

接口文档

image-20250527204631668

image-20250527204708322

image-20250527204730007

Controller层
1
请求方法为Delete,@DeleteMapping({ids})注解;路径参数ids前加上@PathVariable
1
2
3
4
5
6
7
8
@DeleteMapping("/{ids}")
public Result delete(@PathVariable List<Integer> ids){
log.info("批量删除ids:{}",ids);

empservice.delete(ids);

return Result.success();
}
Service层
1
2
3
4
@Override
public void delete(List<Integer> ids) {
empMapper.delete(ids);
}
Mapper层

对应sql:

1
delete from emp where id in (1,2,3)

xml映射文件中:< foreach >实现对数组的遍历

1
2
3
4
5
6
<delete id="delete">
delete from emp where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>

新增员工

接口文档

image-20250527210259743

image-20250527210344910

image-20250527210405378

Controller层
1
2
3
4
5
6
7
//前端发送json请求体,封装为pojo实体类
@PostMapping
public Result save(@RequestBody Emp emp){
empservice.save(emp);

return Result.success();
}
Service层
1
2
3
4
5
6
7
@Override
public void save(Emp emp) {
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());

empMapper.insert(emp);
}
Mapper层
1
void insert(Emp emp);

xml映射文件

1
2
3
<insert id="insert">
insert into emp (username, name, gender, image, job, entrydate, dept_id,create_time,update_time) values (# {username},#{name},#{gender},#{image},#{deptId},#{entryDate},#{job},#{createTime},#{updateTime})
</insert>

查询回显

接口文档

image-20250529143643189

image-20250529143712971

image-20250529143729208

Contoller层
1
2
3
4
5
6
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id){
log.info("查询id:{}",id);

return Result.success(empService.getById(id));
}
Service层
1
2
3
4
@Override
public Emp getById(Integer id) {
return empMapper.selectById(id);
}
Mapper层
1
2
@Select("select* from emp where id=#{id}")
Emp selectById(Integer id);
测试

image-20250529144631670

image-20250529144740720

修改员工

接口文档

image-20250529144855764

image-20250529144913450

image-20250529144930073

Contoller层
1
2
3
4
5
6
7
8
@PutMapping
public Result update(@RequestBody Emp emp){
log.info("要修改的员工:{}",emp);

empService.update(emp);

return Result.success();
}
Service层
1
2
3
4
5
@Override
public void update(Emp emp) {
emp.setUpdateTime(LocalDateTime.now());
empMapper.update(emp);
}
Mapper层
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
<update id="update">
UPDATE emp
<set>
<if test="username != null and username != ''">
username =
#{username},
</if>
<if test="password != null and password != ''">
password =
#{password},
</if>
<if test="name != null and name != ''">
name =
#{name},
</if>
<if test="gender != null">
gender =
#{gender},
</if>
<if test="image != null and image != ''">
image =
#{image},
</if>
<if test="job != null">
job =
#{job},
</if>
<if test="entryDate != null">
entrydate =
#{entryDate},
</if>
<if test="deptId != null">
dept_id =
#{deptId},
</if>
<if test="createTime != null">
create_time =
#{createTime},
</if>
<if test="updateTime != null">
update_time =
#{updateTime}
</if>
</set>
WHERE id = #{id}
</update>
测试

image-20250529151243177

配置文件

对比

image-20250529153509010

application.properties

image-20250529151846216

1
2
3
4
5
6
7
//类中
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
@Value("${aliyun.oss.region}")
private String region;
1
2
3
4
#配置文件application.properties
aliyun.oss.endpoint=https://oss-cn-chengdu.aliyuncs.com
aliyun.oss.bucketName=k4n9l4n
aliyun.oss.region=cn-chengdu

调试

注入成功

image-20250529152727739

yaml

后缀为.yml / .yaml

image-20250529153128171

配置文件命名为application.yaml;出现spring图标,说明被识别

写入:

1
2
server:
port: 9000

测试:

image-20250529153314006

语法

image-20250529153707095

定义对象/Map

1
2
3
User:
name: 淳平
age: 24

定义数组/List/Set

1
2
3
List:
- 114514
- 1919810

改写application.properties

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
spring:
application:
name: Spring-Mybatis-demo
# 数据库连接信息
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_demo
username: root
password: 114514
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB

#mybatis
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true

#aliyun OSS
aliyun:
oss:
endpoint: https://oss-cn-chengdu.aliyuncs.com
bucketName: k4n9l4n
region: cn-chengdu

@ConfigurationProperties注解

问题分析:配置文件项多了,就有很多成员变量要加上@Value注解,很繁琐

image-20250529155439654

1
@ConfigurationProperties(prefix = "前缀")
1
2
3
4
5
6
7
8
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOssProperties {
private String endpoint;
private String bucketName;
private String region;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Component
public class AliOSSUtils {
@Autowired
private AliOssProperties aliOssProperties;

/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws Exception {
String endpoint = aliOssProperties.getEndpoint();
String bucketName = aliOssProperties.getBucketName();
String region = aliOssProperties.getRegion();
}
}

调试

image-20250529160323427

与@Value的区别

1
2
3
#@Value只能一个一个注入外部属性

#@ConfigurationProperties可以批量注入外部属性到bean对象中

登录

登录功能

1
2
-- 用到的sql
select* from emp where username = "xxx" and password = "xxx"

接口文档

image-20250529162704456

image-20250529162802585

image-20250529162830108

image-20250529163032540

Controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@Slf4j
@RequestMapping("/login")
public class LoginController {

@Autowired
private EmpService empService;

@PostMapping
public Result login(@RequestBody Emp emp){
log.info("员工登录:{}",emp);

Emp e = empService.login(emp);

return e==null?Result.error("用户名或密码错误"):Result.success();
}
}

Service层

1
2
3
4
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}

Mapper层

1
2
@Select("select * from emp where username=#{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);

测试:

校验

只这样验证会未授权访问,即使退出登录了,输入原来的url就进入了后台

http是无状态的,请求其他业务时,并不知道用户在登录业务这块到底登录了没有

image-20250529165459437

会话技术

image-20250529165935261

image-20250529170622625

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/cookie")
public class CookieController {
// 获取cookie
@GetMapping("/get")
public Result get(HttpServletRequest request){
Cookie[] cookies = request.getCookies();

for (Cookie cookie : cookies) {
if("test".equals(cookie.getName())){
System.out.println(cookie.toString());
}
}

return Result.success();
}
// 设置cookie
@GetMapping("/set")
public Result set(HttpServletResponse response){
response.addCookie(new Cookie("test","114514"));
return Result.success();
}
}

测试:

image-20250529171354299

image-20250529171610496

image-20250529171517963

优缺点

image-20250529171749062

跨域:

三个维度有一个不同就是跨域

image-20250529171905019

Session

image-20250529172052773

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@Slf4j
@RequestMapping("/session")
public class SessionController {
@GetMapping("/set")
public Result set(HttpSession session){
log.info("Session-ID:{}",session.hashCode());
session.setAttribute("Admin","Admin");
return Result.success();
}

@GetMapping("/get")
public Result get(HttpServletRequest request){
HttpSession session = request.getSession();

log.info("Session-ID:{}",session.hashCode());

Object sessionAttribute = session.getAttribute("Admin");

return Result.success(sessionAttribute);
}
}

测试:

/session/get

image-20250529173000207

响应的key:

image-20250529173147455

/session/set

image-20250529173035271

日志输出

image-20250529173050875

优缺点

会出现同一个浏览器的两个请求获取到不同的sessionID

image-20250529173402494

令牌

image-20250529185829749

JWT令牌

image-20250529190221636

image-20250529190319157

生成

使用的算法

image-20250529190913764

生成jwt的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testGenJWT(){
Map<String,Object> claims = new HashMap<>();

claims.put("id",1);
claims.put("name","淳平");

String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "114514") //算法以及密钥
.addClaims(claims) //自定义内容(Payload)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //设置到期时间,这里设置1h 3600*1000ms
.compact();

System.out.println(jwt);
}

image-20250529192009773

将生成的令牌放到官网进行解码

image-20250529191955632

我们只是简单测试一下jwt的生成,可以注释掉SpringBootTest注解,这样就不会加载整个项目

image-20250529191711807

既然生成了,那我们如何解析呢

解析

1
2
3
4
5
6
7
8
9
@Test
public void parseJwt() {
Claims claims = Jwts.parser()
.setSigningKey("114514") //密钥
.parseClaimsJws("") //前一步生成的令牌,parseClaimsJws千万不要把Jws写错成Jwt
.getBody(); //获取payload部分

System.out.println(claims);
}

image-20250529192851527

1
2
#JWT校验时用到签名密钥,必须和生成JWT令牌时使用的密钥是配套的
#如果JWT令牌解析校验时报错,则JWT令牌被篡改或失效,令牌非法

案例:

接口文档

image-20250529193323545

JWT令牌工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class JWTUtils {

private static String signKey = "114514"; //密钥;
private static Long expire = 43200000L; //12h过期

/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}

/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}

LoginController

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
@RestController
@Slf4j
@RequestMapping("/login")
public class LoginController {

@Autowired
private EmpService empService;

@PostMapping
public Result login(@RequestBody Emp emp){
log.info("员工登录:{}",emp);

Emp e = empService.login(emp);

if(e != null){ //用户登录成功
Map<String,Object> claims = new HashMap<>();
claims.put("id",e.getId());
claims.put("username",e.getUsername());
claims.put("password",e.getPassword());
claims.put("name",e.getName());
claims.put("gender",e.getGender());
claims.put("image",e.getImage());
claims.put("job",e.getJob());

String jwt = JWTUtils.generateJwt(claims);
return Result.success(jwt);
}
return Result.error("用户名或密码错误");
}
}

测试

image-20250529194909979

Filter过滤器

image-20250529195512844

image-20250529195657748

filterdemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;

import java.io.IOException;

@WebFilter(urlPatterns = "/*") //拦截所有
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init方法执行");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("doFilter方法执行");
}

@Override
public void destroy() {
System.out.println("destory方法执行");
}
}

启动类

1
2
3
4
5
6
7
8
9
@ServletComponentScan //要加上这个,因为Filter不是SpringBoot的组件
@SpringBootApplication
public class SpringMybatisDemoApplication {

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

}

测试

运行

image-20250529200352680

发送请求

image-20250529200408141

拦截到了请求

image-20250529200432232

拦截多次

image-20250529200523233

放行

1
2
3
4
5
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("doFilter方法执行");
filterChain.doFilter(servletRequest,servletResponse); //使用这个api进行放行数据
}

销毁

image-20250529200715344

详解

image-20250529200940615

放行后访问对应web资源后,还会再回到filter中,但是不会从头开始执行filter,从chain.filter后执行(只执行放行后逻辑)

拦截路径

image-20250529201536403

过滤器链

image-20250529201848755

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//filter1
@WebFilter(urlPatterns = "/*") //拦截所有
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init方法执行");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter1拦截到请求...放行前");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("Filter1拦截到请求...放行前");
}

@Override
public void destroy() {
System.out.println("destory方法执行");
}
}
1
2
3
4
5
6
7
8
9
10
//filter2
@WebFilter(urlPatterns = "/*")
public class FilterDemo2 implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter2拦截到请求...放行前");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("Filter2拦截到请求...放行前");
}
}

image-20250529202151562

image-20250529202207046

filterChain的doFilter会将请求/响应发送到下一个Filter,如果是最后一个Filter了,就访问web资源/响应数据给浏览器

案例-登录校验

所有被拦截到的请求都要校验令牌吗?login不用,因为这步人家根本没有令牌

拦截请求后,什么时候才可以放行,执行业务操作?有令牌,且令牌合法,否则返回未登录错误信息

image-20250529203043645

alibaba fastjson:将对象转换为json格式

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>

过滤器

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
@Slf4j
@WebFilter(urlPatterns = "/*")
public class WebFilter0 implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//1.获取url
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

StringBuffer url = request.getRequestURL();
//2.排除/login的请求路径
String ext = url.substring(url.lastIndexOf("/"));
log.info("请求路径:{}",ext);

if ("/login".equals(ext)) {
log.info("登陆操作,放行");
filterChain.doFilter(servletRequest, servletResponse);
return;

} else {
//3.获取请求头中的token
String jwt = request.getHeader("token");
//4.判断token存在性,不存在,返回未登录
if (!StringUtils.hasLength(jwt)) { //使用StringUtils工具类判断jwt是否存在
log.info("jwt不存在");
Result result = Result.error("NOT_LOGIN");
//手动将对象转换为json格式 alibaba fastJson
String jsonString = JSONObject.toJSONString(result);
//响应给浏览器
response.getWriter().write(jsonString);
return;
}
//5.token存在,判断合法性,若不合法,返回未登录
try {
Map<String, Object> claims = JWTUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("jwt非法");
Result result = Result.error("NOT_LOGIN");
//手动将对象转换为json格式 alibaba fastJson
String jsonString = JSONObject.toJSONString(result);
//响应给浏览器
response.getWriter().write(jsonString);
return;
}
//6.放行
log.info("令牌校验通过");
filterChain.doFilter(servletRequest, servletResponse);
}
}
}

测试

image-20250529205843699

image-20250529205821793

image-20250529210258306

image-20250529210313027

Interceptor拦截器

类似于Filter,但不同的是,Interceptor是Spring框架提供的

image-20250529211148152

步骤

定义拦截器

image-20250529211846682

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
@Component //后续需要被注册为bean
public class InterceptorTest implements HandlerInterceptor {
/**
* 目标资源方法执行前执行
* @param request
* @param response
* @param handler
* @return true:放行 false:不放行
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...");
return true;
}

/**
* 目标资源方法执行后执行
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
}

/**
* 视图渲染完毕后执行,最后执行
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}

注册拦截器

image-20250529212222108

1
2
3
4
5
6
7
8
9
@Configuration //配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private InterceptorTest interceptorTest;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptorTest).addPathPatterns("/**"); //注册拦截所有的拦截器
}
}

测试

拦截器中三个方法的顺序

image-20250529212546449

详解

拦截路径

image-20250529212831358

1
2
3
4
5
6
7
8
9
10
11
@Configuration //配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private InterceptorTest interceptorTest;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptorTest)
.addPathPatterns("/**") //注册拦截所有路径的拦截器
.excludePathPatterns("/login"); //排除拦截/login
}
}

拦截流程

image-20250529213550109

与Filter的区别

Filter:实现Filter接口,会拦截所有的资源

Interceptor:实现HandlerInterceptor接口,只会拦截Spring环境内的资源

案例

步骤与Filter中一致,不同的是,拦截器是否放行由返回值决定,而过滤器需要手动调用Chain.doFilter()

定义拦截器

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
@Component
@Slf4j
public class SpringInterceptor implements HandlerInterceptor {
//Controller之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取url
StringBuffer url = request.getRequestURL();
log.info("url:{}",url);
String ext = url.substring(url.lastIndexOf("/"));
//2.排除/login的请求路径
if ("/login".equals(ext)) {
log.info("登陆操作,放行");
return true;
}
//3.获取请求头中的token
String jwt = request.getHeader("token");
//4.判断token存在性,不存在,返回未登录
if(!StringUtils.hasLength(jwt)){
log.info("jwt不存在");
Result result = Result.error("NOT_LOGIN");
//手动将对象转换为json格式 alibaba fastJson
String jsonString = JSONObject.toJSONString(result);
//响应给浏览器
response.getWriter().write(jsonString);
return false;
}
//5.token存在,判断合法性,若不合法,返回未登录
try {
Map<String, Object> claims = JWTUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("jwt非法");
Result result = Result.error("NOT_LOGIN");
//手动将对象转换为json格式 alibaba fastJson
String jsonString = JSONObject.toJSONString(result);
//响应给浏览器
response.getWriter().write(jsonString);
return false;
}
//6.放行
log.info("令牌校验通过");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}

注册拦截器

1
2
3
4
5
6
7
8
9
10
@Configuration //配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private SpringInterceptor springInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(springInterceptor)
.addPathPatterns("/**");//注册拦截所有路径的拦截器
}
}

测试
/login

image-20250529214857718

无token进行其他请求

image-20250529214924850

有token且token合法

image-20250529214948362

token非法

image-20250529215007637

异常处理

在案例中,如果在新增部门处新增一个重复的部门,由于MySQL对部门这个字段做了unique约束,所以会引发异常

image-20250603214630788

http状态码为500,服务器端异常

image-20250603214700032

响应数据为

1
{"timestamp":"2025-06-03T13:46:51.851+00:00","status":500,"error":"Internal Server Error","path":"/depts"}

并不是我们预先设定的Result格式

1
2
3
4
5
{
"code":
"msg":""
"data":""
}

所以前端无法处理框架抛出的异常

image-20250603214330999

image-20250603214952621

全局异常处理器

image-20250603215116517

1
2
3
4
5
6
7
8
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)//捕获所有异常
public Result handlerEx(Exception ex){
ex.printStackTrace();
return Result.error("操作失败");
}
}

事务管理

image-20250605204641821

在原本的部门管理中,删除部门就仅仅只是把部门删了,但是该部门下的员工却并未删除

image-20250605205315386

1
2
3
4
@Override
public void delete(Integer id) {
mapper.deleteById(id);
}

如果我们只是简单加上一个根据部门id删除员工的操作,那么如果这两步操作之间出现了异常,就会导致数据不一致,因此这两步操作需要被封装成一个事务

@Transactional

位置:Service层的类/方法/接口上

作用:将当前方法交给spring进行事务管理,方法执行前,开启事务,成功执行完毕,提交事务,否则回滚事务

我们需要在执行多次数据访问操作的方法上使用,查询不影响数据,单步操作如果失败,也会自动回滚

1
2
3
4
5
6
7
@Override
@Transactional
public void delete(Integer id) {
mapper.deleteById(id);
int i = 1/0;//模拟异常
mapper.deleteEmpByDeptId(id);
}

控制台输出事务日志

1
2
3
4
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug

引发异常

image-20250605210529774

但是数据库中数据未发生变化

image-20250605210602745

控制台日志

image-20250605214543960

rollbackFor

@Transactional默认只会回滚RuntimeException及其子类异常,要想回滚其他异常,需要配置rollbackFor属性

1
2
//回滚所有异常
@Transactional(rollbackFor = Exception.class)

propagation

propagation 属性用于定义事务的传播行为,即指定当前方法在执行时如何与已有的事务交互(例如:是否加入已有事务、是否新建事务等)

image-20250605211957791

image-20250605212037642

案例

image-20250605212148057

记录日志这步要放在finally中,这样就可以无论成功失败都能记录

DeptLog对象

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DeptLog {
private LocalDateTime createTime;
private String description;
}

Service层

1
2
3
4
5
6
7
8
9
10
@Service
public class DeptLogImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Override
@Transactional
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}

Mapper层

1
2
3
4
5
@Mapper
public interface DeptLogMapper {
@Insert("insert into dept_log values (#{createTime},#{description})")
void insert(DeptLog deptLog);
}

但是这么做,dept_log表中并不会添加日志

控制台日志中

image-20250605215022842

向dept_log表中插入数据的事务是直接加入到当前删除部门的事务的,所以当删除部门的方法遇到异常回滚时,向dept_log表中插入数据这个动作也回滚了,就没有插入数据

将service层改为

1
2
3
4
5
6
7
8
9
10
@Service
public class DeptLogImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}

控制台日志

删除部门的事务被挂起

image-20250605215539230

完成插入日志信息的事务后,再完成删除部门的事务

image-20250605215652012

删除部门回滚

image-20250605215721578

插入数据成功

image-20250605215408118

使用场景

image-20250605215900731

AOP

定义

面向特定方法编程

要统计业务中每一个方法的耗时,如果在每一个方法中都用开始时间减去运行结束时间的话,太繁琐

image-20250627224200458

image-20250627224530358

image-20250627224508448

步骤

引入依赖

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

编写AOP程序

使用@Component将类交给IOC容器管理,使用@Aspect声明这是一个AOP类

image-20250627225506096

使用固定参数ProceedingJoinPoint proceedingJoinPoint封装原始方法,可用.getSignature()获取方法的签名

使用切入层表达式:@Around(“execution(返回值类型 包名.目录(哪一层).类/接口名.方法名(..))”)对方法进行注解,表示要面向哪些类/接口下的哪些方法

*表示通配

面向Service层下的所有接口和类的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@Component
@Aspect
public class TimeAspect {
@Around("execution(* com.wwwtty.springmybatisdemo.Service.*.*(..))")
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long begin = System.currentTimeMillis();
Object ret = proceedingJoinPoint.proceed();
long end = System.currentTimeMillis();
log.info(proceedingJoinPoint.getSignature()+"运行耗时:{}ms",end-begin);
return ret;
}
}

此时开启服务,进行测试

前端刷新页面时

image-20250627231221301

AOP类给出日志输出,获取了方法的签名

image-20250627231252594

image-20250627231605514

在事务管理中其实就使用了AOP,方法执行前先开启事务,方法结束后commit/rollback

核心概念

连接点JointPoint

可以被AOP控制的方法(暗含方法执行时的相关信息)

通知Advice

在AOP类中做什么(比如前面的统计时间);重复的逻辑(共性功能,最终体现为一个方法)

切入点PointCut

AOP实际控制的方法;匹配连接点的条件,通知仅在切入点方法执行时才被应用

切面Aspect

切入点+通知

目标对象Target

通知所应用的对象

执行流程

image-20250627232704762

打上断点进行测试/depts查询所有员工信息这个接口

image-20250627233203888

image-20250627233222224

image-20250627233601993

没加入AOP时,应该会执行到DeptService实现类中的list()方法

现在加了AOP时,进行测试

image-20250627233335906

方法变成了另外一个增强的list()方法

在Service层这里,并没有直接执行mapper.list()

image-20250627233649477

而是转到AOP类中

image-20250627233718535

执行到proceedingJoinPoint.proceed()调用原方法时,才进入原来Service层中的mappe.list()

image-20250627233839864

通知类型

!image-20250701094154119image-20250701094154119

@Around,如果原方法抛出异常,那么后面的通知就不会执行了

@Around需要考虑原方法调用,因此需要使用proceedingJoinPoint.proceed()调用原方法,同时返回值必须是Object

测试代码:

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
package com.wwwtty.springmybatisdemo.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class AdviceTest {
@Before("execution(* com.wwwtty.springmybatisdemo.Service.*.*(..))")
public void before() {
log.info("Before:Hello World!");
}

@Around("execution(* com.wwwtty.springmybatisdemo.Service.DeptService.list()))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around-before...");
Object res = proceedingJoinPoint.proceed();
log.info("around-after...");
return res;
}

@After("execution(* com.wwwtty.springmybatisdemo.Service.DeptService.list()))")
public void after() {
log.info("after");
}

@AfterReturning("execution(* com.wwwtty.springmybatisdemo.Service.DeptService.list())")
public void afterReturn() {
log.info("after-return");
}

@AfterThrowing("execution(* com.wwwtty.springmybatisdemo.Service.DeptService.*(..))")
public void afterThrow() {
log.info("after-throw");
}
}

Service层代码

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
@Service
public class DeptImpl implements DeptService {
@Autowired
private DeptMapper mapper;
@Autowired
private DeptLogService deptLogService;

@Override
public List<Dept> list() {
return mapper.list();
}

@Override
@Transactional(rollbackFor = Exception.class)
// @Transactional
public void delete(Integer id) {
try {
mapper.deleteById(id);
int i = 1/0;//模拟异常
mapper.deleteEmpByDeptId(id);
} finally {
DeptLog deptLog = new DeptLog(LocalDateTime.now(),"解散了"+id+"部门");
deptLogService.insert(deptLog);
}
}
@Override
public void add(Dept dept){
dept.setCreateTime(LocalDate.now());
dept.setUpdateTime(LocalDate.now());

mapper.insert(dept);
}
}

日志输出结果

image-20250701100901641

image-20250701100922413

image-20250701101006603

但是这样写切入点表达式太麻烦了,写一个注解就要写一个表达式

@Pointcut

使用这个注解就可以抽取出重复的切入点表达式,只需要在通知注解中调用被@Pointcut注解的方法就可以

1
2
3
@Pointcut("execution(* com.wwwtty.springmybatisdemo.Service.DeptService.*(..))")
private void cut(){
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.wwwtty.springmybatisdemo.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class AdviceTest {
@Pointcut("execution(* com.wwwtty.springmybatisdemo.Service.DeptService.*(..))")
private void cut(){
}

@Before("cut()")
public void before() {
log.info("Before:Hello World!");
}

@Around("cut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around-before...");
Object res = proceedingJoinPoint.proceed();
log.info("around-after...");
return res;
}

@After("cut()")
public void after() {
log.info("after");
}

@AfterReturning("cut()")
public void afterReturn() {
log.info("after-return");
}

@AfterThrowing("cut()")
public void afterThrow() {
log.info("after-throw");
}
}

通知顺序

image-20250701102020089

切入点表达式

决定哪些方法要加入通知

image-20250701154605796

@execution()

image-20250701155056994

image-20250701155751571

image-20250701155850321

@annotation

切入点是使用特定注解的方法,使用特定注解的引用对切入点表达式进行简化

1.定义特定注解,使用两个元注解对生效范围和时间进行声明

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}

2.在特定方法上注解

1
2
3
4
5
@Override
@MyLog
public List<Dept> list() {
return mapper.list();
}

3.在@annotation中传入注解的引用

1
2
3
4
5
6
7
8
@Pointcut("@annotation(com.wwwtty.springmybatisdemo.aop.MyLog)")
private void cut(){
}

@Before("cut()")
public void before() {
log.info("Before:Hello World!");
}

连接点

image-20250701162229695

1
2
3
4
5
6
7
8
9
10
@Pointcut("@annotation(com.wwwtty.springmybatisdemo.aop.MyLog)")
private void cut(){
}

@Before("cut()")
public void before(JoinPoint joinPoint) {
log.info("方法签名:{}",joinPoint.getSignature());
log.info("方法参数列表:{}", joinPoint.getArgs());
log.info("目标类名:{}",joinPoint.getClass().getName());
}

案例

image-20250701163039200

image-20250701163602909

image-20250701163615975

分析:

  • 使用AOP技术,对原始增删改查进行增强,要求记录返回值,通知使用@Around进行控制
  • 增删改查的方法命名不同类各不相同,如果使用execution,不好命名,使用@annotation进行描述切入点表达式

引入依赖

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

建表

1
2
3
4
5
6
7
8
9
10
11
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_user int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

实体类

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@NoArgsConstructor
@AllArgsConstructor
public class operateLog {
private Integer id;
private LocalDateTime operateTime;
private String className;
private String methodName;
private String methodParams;
private String returnValue;
private Long costTime;
}

Mapper接口

1
2
3
4
5
@Mapper
public interface OperateLogMapper {
@Insert("insert into operate_Log (id,operate_user,operate_time,class_name,method_name,method_params,return_value,cost_time) values (#{id},#{operateTime},#{className},#{methodName},#{methodParams},#{returnValue},#{costTime})")
void insert(OperateLog operateLog);
}

自定义注解

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
}

设计AOP类

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
@Slf4j
@Component
@Aspect
public class LogAspect {
@Autowired
private HttpServletRequest httpRequest;
@Autowired
private OperateLogMapper mapper;

@Around("@annotation(com.wwwtty.springmybatisdemo.anno.Log)")
public Object record(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始时间
long begin = System.currentTimeMillis();

Object retValue = proceedingJoinPoint.proceed();

OperateLog operateLog = new OperateLog();
//写入日志信息
mapper.insert(operateLog);

//结束时间
long end = System.currentTimeMillis();
//花费时间
long costTime = end - begin;
return retValue;
}
}

AOP中一些细节

如何获取operateLog对象呢(内部需要id等参数)

每次请求时,请求头中都会附上token字段;而token字段是登录时将员工id等信息生成的jwt令牌,我们就使用请求头来进行获取

首先需要一个HttpServletRequest对象

1
2
@Autowired
private HttpServletRequest httpRequest;

通过请求头的token字段获取jwt令牌

1
2
3
4
//登录用户id,通过token获取,需要注入一个请求头获取token字段
String token = httpRequest.getHeader("token");
Claims claims = JWTUtils.parseJWT(token);
Integer operateUser = (Integer) claims.get("id");

调用原方法的返回值是Object,而表中是varchar,转换成JSON存入表中,使用fastJSON

1
2
3
4
//调用原方法
Object res = proceedingJoinPoint.proceed();
//返回值
String retValue = JSONObject.toJSONString(res);

切入点设置

增删改查方法加上@Log注解

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
@Service
public class DeptImpl implements DeptService {
@Autowired
private DeptMapper mapper;
@Autowired
private DeptLogService deptLogService;

@Override
@Log
public List<Dept> list() {
return mapper.list();
}

@Override
@Log
@Transactional(rollbackFor = Exception.class)
// @Transactional
public void delete(Integer id) {
try {
mapper.deleteById(id);
int i = 1/0;//模拟异常
mapper.deleteEmpByDeptId(id);
} finally {
DeptLog deptLog = new DeptLog(LocalDateTime.now(),"解散了"+id+"部门");
deptLogService.insert(deptLog);
}
}
@Override
@Log
public void add(Dept dept){
dept.setCreateTime(LocalDate.now());
dept.setUpdateTime(LocalDate.now());

mapper.insert(dept);
}
}

整体AOP代码

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
@Slf4j
@Component
@Aspect
public class LogAspect {
@Autowired
private HttpServletRequest httpRequest;
@Autowired
private OperateLogMapper mapper;

@Around("@annotation(com.wwwtty.springmybatisdemo.anno.Log)")
public Object record(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始时间
long begin = System.currentTimeMillis();

//登录用户id,通过token获取,需要注入一个请求头获取token字段
String token = httpRequest.getHeader("token");
Claims claims = JWTUtils.parseJWT(token);
Integer operateUser = (Integer) claims.get("id");

//操作时间
LocalDateTime operateTime = LocalDateTime.now();
//操作的类名
String className = proceedingJoinPoint.getTarget().getClass().getName();
//操作的方法名
String methodName = proceedingJoinPoint.getSignature().getName();
//操作的方法的参数列表
String methodParams= Arrays.toString(proceedingJoinPoint.getArgs());

//调用原方法
Object res = proceedingJoinPoint.proceed();
//返回值
String retValue = JSONObject.toJSONString(res);

//结束时间
long end = System.currentTimeMillis();
//花费时间
long costTime = end - begin;
log.info("耗时:{}ms",costTime);
OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,retValue,costTime);
//写入日志信息
mapper.insert(operateLog);

log.info("AOP写入日志:{}",operateLog);
return res;
}
}

配置优先级

配置文件优先级

.properties > .yml > .yaml

image-20250701201109709

image-20250701202433218

image-20250701202410003

虚拟机选项-Dserver.port=

程序实参--server.port=

Bean的管理

image-20250701204314249

IOC容器对象

ApplicationContext封装了IOC容器对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void beanTest() {
//根据bean名称获取
DeptService bean1 = (DeptService) applicationContext.getBean("deptImpl");
System.out.println(bean1);

//根据bean类型获取
DeptController bean2 = applicationContext.getBean(DeptController.class);
System.out.println(bean2);

//根据bean的名称和类型
DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
System.out.println(bean3);
}

bean的作用域

image-20250702092859995

默认情况下

DeptController类

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
@Slf4j
@RestController
@RequestMapping("/depts")
@Scope("")
public class DeptController {
@Autowired
private DeptService service;
@GetMapping
public Result list() {
log.info("查询所有部门信息");
//查询所有部门信息
List<Dept> deptList = service.list();
return Result.success(deptList);
}

@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id){
log.info("根据id删除部门:{}",id);
service.delete(id);
return Result.success();
}

@PostMapping
public Result add(@RequestBody Dept dept){
log.info("新增部门{}",dept);
service.add(dept);
return Result.success();
}

public DeptController() {
System.out.println("DeptController");
}
}

测试方法

1
2
3
4
5
6
@Test
public void scopeTest(){
for (int i = 0; i < 10; i++) {
System.out.println(applicationContext.getBean(DeptController.class));
}
}

运行结果

bean对象在程序开始运行时就new好了

image-20250702093058240

输出bean的信息

是同一个实例

image-20250702093142112

@Lazy

使用该注解后,bean对象在第一次使用前才会被初始化

image-20250702093946209

@Scope

该注解可以用于指定bean的作用域

指定作用域为prototype

1
@Scope("prototype")

运行结果,每个bean对象都不同

image-20250702094352774

第三方bean

第三方提供的对象,我们不能在对应的类上直接加上@Component及其衍生注解,此时我们需要使用:

@Bean

方法1(不推荐):

在SpringBooot启动类中,定义一个返回值为第三方对象的方法,在这个方法上加上注解@Bean

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class SpringMybatisDemoApplication {
@Bean
public SAXReader saxReader(){
return new SAXReader();
}
public static void main(String[] args) {
SpringApplication.run(SpringMybatisDemoApplication.class, args);
}
}

方法2:单独创建一个配置类

1
2
3
4
5
6
7
@Configuration
public class CommonConfig {
@Bean
public SAXReader saxReader(){
return new SAXReader();
}
}

@Bean注解下bean对象的名称

默认是方法名,可以通过@Bean的value/name属性进行命名

第三方bean依赖注入

之前我们都是使用@Autowired进行注入,但在第三方bean中,我们可以直接在方法中传入参数,参数会被IOC容器自动装配

1
2
3
4
5
6
7
8
@Configuration
public class CommonConfig {
@Bean
public SAXReader saxReader(DeptController deptController){
System.out.println(deptController);
return new SAXReader();
}
}

image-20250702100733579

SpringBoot原理

image-20250702101053173

起步依赖

image-20250702101221263

自动配置

image-20250702101533764

引入第三方依赖

image-20250702102532862

导入第三方的pom.xml

image-20250702102615889

进行测试,看看Bean有没有正确添加到IOC容器中

image-20250702102837199

1
2
3
4
@Test
public void pomTest(){
applicationContext.getBean(TokenParser.class);
}

测试不通过

image-20250702103014253

原因是SpringBoot启动类的注解扫描bean对象只能在本包下以及子包,TokenParser并没有和启动类在一个包下

方法1:用@ComponentScan

使用了@ComponentScan会覆盖原先@SpringBootApplication扫描当前包的行为,所以需要声明2个包

image-20250702103600690

1
2
3
4
5
6
7
8
@ServletComponentScan
@SpringBootApplication
@ComponentScan({"com.example","com.wwwtty.springmybatisdemo"})
public class SpringMybatisDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringMybatisDemoApplication.class, args);
}
}

这下就可以测试通过了

缺点是太繁琐

方案2:@Import导入

image-20250702104841837

1
2
3
4
5
6
7
8
9
@ServletComponentScan
@SpringBootApplication
@Import({TokenParser.class, HeaderConfig.class})
//@ComponentScan({"com.example","com.wwwtty.springmybatisdemo"})
public class SpringMybatisDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringMybatisDemoApplication.class, args);
}
}

还是比较麻烦,要记住导入的类都是什么类型的

方案3:封装@Import

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)
public @interface EnableHeaderConfig {
}

ImportSelector的实现类

1
2
3
4
5
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.example.HeaderConfig","com.example.TokenParser"};
}
}