JavaWeb后端开发:SpringBoot案例

SpringBoot案例

准备工作

1.准备数据库表(deptemp)。

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
-- 部门管理
create table dept(
id int unsigned primary key auto_increment comment '主键ID',
name varchar(10) not null unique comment '部门名称',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '部门表';

insert into dept (id, name, create_time, update_time) values(1,'学工部',now(),now()),(2,'教研部',now(),now()),(3,'咨询部',now(),now()), (4,'就业部',now(),now()),(5,'人事部',now(),now());



-- 员工管理(带约束)
create table emp (
id int unsigned primary key auto_increment comment 'ID',
username varchar(20) not null unique comment '用户名',
password varchar(32) default '123456' comment '密码',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
image varchar(300) comment '图像',
job tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
entrydate date comment '入职时间',
dept_id int unsigned comment '部门ID',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '员工表';

INSERT INTO emp
(id, username, password, name, gender, image, job, entrydate,dept_id, create_time, update_time) VALUES
(1,'jinyong','123456','金庸',1,'1.jpg',4,'2000-01-01',2,now(),now()),
(2,'zhangwuji','123456','张无忌',1,'2.jpg',2,'2015-01-01',2,now(),now()),
(3,'yangxiao','123456','杨逍',1,'3.jpg',2,'2008-05-01',2,now(),now()),
(4,'weiyixiao','123456','韦一笑',1,'4.jpg',2,'2007-01-01',2,now(),now()),
(5,'changyuchun','123456','常遇春',1,'5.jpg',2,'2012-12-05',2,now(),now()),
(6,'xiaozhao','123456','小昭',2,'6.jpg',3,'2013-09-05',1,now(),now()),
(7,'jixiaofu','123456','纪晓芙',2,'7.jpg',1,'2005-08-01',1,now(),now()),
(8,'zhouzhiruo','123456','周芷若',2,'8.jpg',1,'2014-11-09',1,now(),now()),
(9,'dingminjun','123456','丁敏君',2,'9.jpg',1,'2011-03-11',1,now(),now()),
(10,'zhaomin','123456','赵敏',2,'10.jpg',1,'2013-09-05',1,now(),now()),
(11,'luzhangke','123456','鹿杖客',1,'11.jpg',5,'2007-02-01',3,now(),now()),
(12,'hebiweng','123456','鹤笔翁',1,'12.jpg',5,'2008-08-18',3,now(),now()),
(13,'fangdongbai','123456','方东白',1,'13.jpg',5,'2012-11-01',3,now(),now()),
(14,'zhangsanfeng','123456','张三丰',1,'14.jpg',2,'2002-08-01',2,now(),now()),
(15,'yulianzhou','123456','俞莲舟',1,'15.jpg',2,'2011-05-01',2,now(),now()),
(16,'songyuanqiao','123456','宋远桥',1,'16.jpg',2,'2007-01-01',2,now(),now()),
(17,'chenyouliang','123456','陈友谅',1,'17.jpg',NULL,'2015-03-21',NULL,now(),now());

2.创建springboot工程,引入对应的起步依赖(webmybatismysql驱动、lombok)。

3.配置文件application.properties中引入mybatis的配置信息,准备对应的实体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置数据库的连接信息 - 四要素
# 驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据库连接的url(mybatis是数据库的名称)
spring.datasource.url=jdbc:mysql://localhost:3306/tlias
# 连接数据库的用户名
spring.datasource.username=root
# 连接数据库的密码
spring.datasource.password=
# 注意:我的root用户密码为空,所有不用填写password,如果密码不为空需要在=号后面填写密码

#开启驼峰命名自动映射,即从数据库字段名 a_column 映射到Java属性名 aColumn。
#开启mybatis的驼峰命名自动映射开关 a_column ------> aCloumn
mybatis.configuration.map-underscore-to-camel-case=true
1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.pojo;
//部门实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
private Integer id; //ID
private String name; //部门名称
private LocalDateTime createTime; //创建时间
private LocalDateTime updateTime; //修改时间
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itheima.pojo;
//员工实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
private Integer id; //ID
private String username; //用户名
private String password; //密码
private String name; //姓名
private Short gender; //性别 , 1 男, 2 女
private String image; //图像url
private Short job; //职位 , 1 班主任 , 2 讲师 , 3 学工主管 , 4 教研主管 , 5 咨询师
private LocalDate entrydate; //入职日期
private Integer deptId; //部门ID
private LocalDateTime createTime; //创建时间
private LocalDateTime updateTime; //修改时间
}

4.准备对应的MapperService(接口、实现类)、Controller基础结构。

开发规范

Restful

传统风格:

1
2
3
4
http://localhost:8080/user/getById?id=1     GET:查询id为1的用户
http://localhost:8080/user/saveUser POST:新增用户
http://localhost:8080/user/updateUser POST:修改用户
http://localhost:8080/user/deleteUser?id=1 GET:删除id为1的用户

RESTREpresentational State Transfer),表述性状态转换,它是一种软件架构风格。

REST风格:URL定位资源,HTTP动词描述操作。

1
2
3
4
http://localhost:8080/users/1  GET:查询id为1的用户
http://localhost:8080/users POST:新增用户
http://localhost:8080/users PUT:修改用户
http://localhost:8080/users/1 DELETE:删除id为1的用户

注意:

  • REST是风格,是约定方式,约定不是规定,可以打破。
  • 描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:usersempsbook
统一响应结果

前后端交互统一响应结果Result

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.itheima.pojo;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;//响应码,1 代表成功; 0 代表失败
private String msg; //响应信息 描述字符串
private Object data; //返回的数据

//增删改 成功响应
public static Result success(){
return new Result(1,"success",null);
}
//查询 成功响应
public static Result success(Object data){
return new Result(1,"success",data);
}
//失败响应
public static Result error(String msg){
return new Result(0,msg,null);
}
}

部门管理

查询部门

请求路径:/depts

请求方式:GET

接口描述:该接口用于部门列表数据查询

接口测试:http://localhost:8080/depts

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

@Slf4j
@RestController
public class DeptController {
@Autowired
private DeptService deptService;

//private static Logger log = LoggerFactory.getLogger(DeptController.class);
//定义log变量,或者使用@Slf4j注解

//@RequestMapping(value = "/depts", method = RequestMethod.GET)//指定请求方式为GET
@GetMapping("/depts")
public Result depts() {
log.info("查询全部部门数据");
List<Dept> deptList = deptService.list();
return Result.success(deptList);
}
}
1
2
3
4
5
6
package com.itheima.service;

public interface DeptService {
//查询全部部门数据
List<Dept> list();
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.service.impl;

@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;

@Override
public List<Dept> list() {
return deptMapper.list();
}
}
1
2
3
4
5
6
7
8
package com.itheima.mapper;

@Mapper
public interface DeptMapper {
//查询全部部门数据
@Select("select * from dept")
List<Dept> list();
}
前后端联调

将前端工程文件夹中的压缩包,拷贝到一个没有中文不带空格的目录下,解压。启动nginx,访问测试:http://localhost:90

前端请求路径:http://localhost:90/api/depts,`localhost:90/api/`代表的是请求`nginx`路径,`nginx`服务器接收到请求之后,把该请求转给后端的`8080`端口`Tomcat`,最终由`Tomcat`处理这次请求。

删除部门

请求路径:/depts/{id}

请求方式:DELETE

接口描述:该接口用于根据ID删除部门数据

接口测试:http://localhost:8080/depts/1

1
2
3
4
5
6
7
8
//DeptController
//删除部门
@DeleteMapping("depts/{id}")
public Result delete(@PathVariable Integer id) {
log.info("根据id删除部门:{}", id);
deptService.delete(id);
return Result.success();
}
1
2
3
//DeptService
//删除部门
void delete(Integer id);
1
2
3
4
5
6
//DeptServiceImpl
//删除部门
@Override
public void delete(Integer id) {
deptMapper.deleteById(id);
}
1
2
3
4
//DeptMapper
//删除部门
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);
新增部门

请求路径:/depts

请求方式:POST

接口描述:该接口用于添加部门数据

接口测试:http://localhost:8080/depts

请求参数样例:

1
2
3
{
"name" : "部"
}

注意:一个完整的请求路径,是类上的@RequestMappingvalue属性 + 方法上的@RequestMappingvalue属性。

1
2
3
4
5
6
7
8
//DeptController
//新增部门
@PostMapping("depts")
public Result add(@RequestBody Dept dept) {
log.info("新增部门:{}", dept);
deptService.add(dept);
return Result.success();
}
1
2
3
//DeptService
//新增部门
void add(Dept dept);
1
2
3
4
5
6
7
8
//DeptServiceImpl
//新增部门
@Override
public void add(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.insert(dept);
}
1
2
3
4
//DeptMapper
//新增部门
@Insert("insert into dept(name, create_time, update_time) values (#{name}, #{createTime}, #{updateTime})")
void insert(Dept dept);
根据ID查询

请求路径:/depts/{id}

请求方式:GET

接口描述:该接口用于根据ID查询部门数据

接口测试:http://localhost:8080/depts/1

1
2
3
4
5
6
7
8
//DeptController
//根据ID查询部门
@GetMapping("depts/{id}")
public Result get(@PathVariable Integer id) {
log.info("根据ID查询:{}", id);
Dept dept = deptService.get(id);
return Result.success(dept);
}
1
2
3
//DeptService
//根据ID查询部门
Dept get(Integer id);
1
2
3
4
5
6
7
//DeptServiceImpl
//根据ID查询部门
@Override
public Dept get(Integer id) {
Dept dept = deptMapper.getById(id);
return dept;
}
1
2
3
4
//DeptMapper
//根据ID查询部门
@Select("select * from dept where id = #{id}")
Dept getById(Integer id);
修改部门

请求路径:/depts

请求方式:PUT

接口描述:该接口用于修改部门数据

接口测试:http://localhost:8080/depts

请求参数样例:

1
2
3
4
{
"id": 1,
"name": "其他部"
}
1
2
3
4
5
6
7
8
//DeptController
//修改部门
@PutMapping("depts")
public Result update(@RequestBody Dept dept) {
log.info("修改部门:{}", dept);
deptService.update(dept);
return Result.success();
}
1
2
3
//DeptService
//修改部门
void update(Dept dept);
1
2
3
4
5
6
7
8
9
//DeptServiceImpl
//修改部门
@Override
public void update(Dept dept) {
Dept dept_new = get(dept.getId());
dept_new.setName(dept.getName());
dept_new.setUpdateTime(LocalDateTime.now());
deptMapper.update(dept_new);
}
1
2
3
4
//DeptMapper
//修改部门
@Update("update dept set name = #{name}, update_time = #{updateTime} where id = #{id}")
void update(Dept dept1);

员工管理

分页查询

请求路径:/emps

请求方式:GET

接口描述:该接口用于员工列表数据的(条件)分页查询

请求参数样例:(参数格式:queryString)

1
/emps?page=1&pageSize=10

【注意】使用@RequestParampagepageSize设置默认值后,请求参数可以不填写这两个值。

请求参数:页码、每页展示记录数。

响应结果:总记录数、结果列表 (PageBean)。

注意:@RequestParam的属性defaultValue可以来设置参数的默认值。

1
2
3
4
5
6
7
8
9
10
package com.itheima.pojo;

//分页查询结果封装类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
private long total;//总记录数
private List rows;//数据列表,这里的List不写泛型,因为开发中不一定传的是emp,还可以是其他类
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itheima.controller;

@Slf4j
@RestController
public class EmpController {
@Autowired
private EmpService empService;

//分页查询
@GetMapping("emps")
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("分页查询,参数:{},{}", page, pageSize);
PageBean pageBean = empService.page(page, pageSize);
return Result.success(pageBean);
}
}
1
2
3
4
5
6
package com.itheima.service;

public interface EmpService {
//分页查询
PageBean page(Integer page, Integer pageSize);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.itheima.service.impl;

@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;

@Override
public PageBean page(Integer page, Integer pageSize) {//分页查询
//1.获取总记录数
Long count = empMapper.count();

//2.根据分页查询结果列表
Integer start = (page - 1) * pageSize;
List<Emp> empList = empMapper.page(start, pageSize);

//3.封装PageBean对象
PageBean pageBean = new PageBean(count, empList);
return pageBean;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.mapper;

@Mapper
public interface EmpMapper {
//查询总记录数
@Select("select count(*) from emp")
public Long count();

//分页查询,获取列表数据
@Select("select * from emp limit #{start}, #{pageSize}")
public List<Emp> page(Integer start, Integer pageSize);
}
分页插件PageHelper

pom.xml文件中引入分页插件:

1
2
3
4
5
6
<!--PageHelper分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>

只需要修改EmpServiceImpl.javaEmpMapper.java两个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//EmpServiceImpl
//使用PageHelper实现员工信息查询
@Override
public PageBean page(Integer page, Integer pageSize) {
//1.设置分页参数
PageHelper.startPage(page, pageSize);

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

//3.封装PageBean对象
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
1
2
3
4
//EmpMapper
//员工信息查询
@Select("select * from emp")
public List<Emp> list();

测试链接:http://localhost:8080/emps?page=2&pageSize=10

输出日志:虽然只调用了list方法的select * from emp语句,但是还是分别执行了SELECT count(0) FROM empselect * from emp LIMIT ?, ?这两个操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JDBC Connection [HikariProxyConnection@935528035 wrapping com.mysql.cj.jdbc.ConnectionImpl@7b18ae7] will not be managed by Spring
==> Preparing: SELECT count(0) FROM emp
==> Parameters:
<== Columns: count(0)
<== Row: 17
<== Total: 1
==> Preparing: select * from emp LIMIT ?, ?
==> Parameters: 10(Long), 10(Integer)
<== Columns: id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time
<== Row: 11, luzhangke, 123456, 鹿杖客, 1, 11.jpg, 5, 2007-02-01, 3, 2024-10-20 21:55:12, 2024-10-20 21:55:12
<== Row: 12, hebiweng, 123456, 鹤笔翁, 1, 12.jpg, 5, 2008-08-18, 3, 2024-10-20 21:55:12, 2024-10-20 21:55:12
<== Row: 13, fangdongbai, 123456, 方东白, 1, 13.jpg, 5, 2012-11-01, 3, 2024-10-20 21:55:12, 2024-10-20 21:55:12
<== Row: 14, zhangsanfeng, 123456, 张三丰, 1, 14.jpg, 2, 2002-08-01, 2, 2024-10-20 21:55:12, 2024-10-20 21:55:12
<== Row: 15, yulianzhou, 123456, 俞莲舟, 1, 15.jpg, 2, 2011-05-01, 2, 2024-10-20 21:55:12, 2024-10-20 21:55:12
<== Row: 16, songyuanqiao, 123456, 宋远桥, 1, 16.jpg, 2, 2007-01-01, 2, 2024-10-20 21:55:12, 2024-10-20 21:55:12
<== Row: 17, chenyouliang, 123456, 陈友谅, 1, 17.jpg, null, 2015-03-21, null, 2024-10-20 21:55:12, 2024-10-20 21:55:12
<== Total: 7
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@32425622]
条件分页查询

和分页查询一致。

请求路径:/emps

请求方式:GET

接口描述:该接口用于员工列表数据的(条件)分页查询

请求参数样例:(参数格式:queryString)

1
/emps?name=张&gender=1&begin=2007-09-01&end=2022-09-01&page=1&pageSize=10

1
2
3
4
5
6
7
8
9
10
11
12
//EmpController
//条件分页查询:在原来分页查询的基础上增加了查询条件参数
@GetMapping("emps")
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
String name, Short gender,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
log.info("分页查询,参数:{},{},{},:{},{},{}", page, pageSize, name, gender, begin, end);
PageBean pageBean = empService.page(page, pageSize, name, gender, begin, end);
return Result.success(pageBean);
}
1
2
3
//EmpService
//条件分页查询:在原来分页查询的基础上增加了查询条件参数
PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//EmpServiceImpl
//条件分页查询:在原来分页查询的基础上增加了查询条件参数
@Override
public PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {
//1.设置分页参数
PageHelper.startPage(page, pageSize);

//2.执行查询
List<Emp> empList = empMapper.list(name, gender, begin, end);
Page<Emp> p = (Page<Emp>) empList;

//3.封装PageBean对象
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
1
2
3
4
//EmpMapper
//条件分页查询:在原来分页查询的基础上增加了查询条件参数
//要使用if判断条件,所以使用动态SQL,用XML映射
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">
<!-- 条件查询员工 -->
<select id="list" resultType="com.itheima.pojo.Emp">
select * from emp
<where>
<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>
order by update_time desc
</select>
</mapper>
删除员工

请求路径:/emps/{ids}

请求方式:DELETE

接口描述:该接口用于批量删除员工的数据信息

请求参数样例:http://localhost:8080/emps/21,22

1
2
3
4
5
6
7
8
//EmpController
//批量删除员工,传入的多个id用逗号分隔,可以被识别为数组
@DeleteMapping("emps/{ids}")
public Result delete(@PathVariable() List<Integer> ids) {
log.info("批量删除操作,ids = {}", ids);
empService.delete(ids);
return Result.success();
}
1
2
3
//EmpService
//批量删除员工
void delete(List<Integer> ids);
1
2
3
4
5
6
//EmpServiceImpl
//批量删除员工
@Override
public void delete(List<Integer> ids) {
empMapper.delete(ids);
}
1
2
3
//EmpMapper
//批量删除员工
void delete(List<Integer> ids);
1
2
3
4
5
6
7
8
<!--  批量删除员工  -->
<delete id="delete">
delete from emp
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
新增员工

请求路径:/emps

请求方式:POST

接口描述:该接口用于添加员工的信息

请求参数样例:

1
2
3
4
5
6
7
8
9
{
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
"username": "linpingzhi",
"name": "林平之",
"gender": 1,
"job": 1,
"entrydate": "2022-09-18",
"deptId": 1
}

1
2
3
4
5
6
7
8
//EmpController
//新增员工
@PostMapping("emps")
public Result save(@RequestBody Emp emp) {
log.info("新增员工,emp:{}", emp);
empService.save(emp);
return Result.success();
}
1
2
3
//EmpService
//新增员工
void save(Emp emp);
1
2
3
4
5
6
7
8
//EmpServiceImpl
//新增员工
@Override
public void save(Emp emp) {
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
}
1
2
3
4
5
//EmpMapper
//新增员工
@Insert("insert into emp (username, name, gender, image, job, entrydate, dept_id, create_time, update_time) " +
"values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
void insert(Emp emp);
根据ID查询员工(查询回显)

请求路径:/emps/{id}

请求方式:GET

接口描述:该接口用于根据主键ID查询员工的信息

请求参数样例:http://localhost:8080/emps/23

1
2
3
4
5
6
7
8
//EmpController
//根据ID查询员工
@GetMapping("emps/{id}")
public Result getById(@PathVariable() Integer id) {
log.info("根据id查询员工信息,id:{}", id);
Emp emp = empService.getById(id);
return Result.success(emp);
}
1
2
3
//EmpService
//根据ID查询员工
Emp getById(Integer id);
1
2
3
4
5
6
7
//EmpServiceImpl
//根据ID查询员工
@Override
public Emp getById(Integer id) {
Emp emp = empMapper.getById(id);
return emp;
}
1
2
3
4
//EmpMapper
//根据ID查询员工
@Select("select * from emp where id = #{id}")
Emp getById(Integer id);
修改员工

请求路径:/emps

请求方式:PUT

接口描述:该接口用于修改员工的数据信息

请求参数样例:

1
2
3
4
5
6
7
8
9
10
11
{
"id": 25,
"password": 888888,
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
"username": "linpingzhi",
"name": "林平之",
"gender": 1,
"job": 1,
"entrydate": "2022-09-18",
"deptId": 1
}

1
2
3
4
5
6
7
8
//EmpController
//更新员工
@PutMapping("emps")
public Result update(@RequestBody Emp emp) {
log.info("更新员工信息:{}", emp);
empService.update(emp);
return Result.success();
}
1
2
3
//EmpService
//更新员工
void update(Emp emp);
1
2
3
4
5
6
7
//EmpServiceImpl
//更新员工
@Override
public void update(Emp emp) {
emp.setUpdateTime(LocalDateTime.now());
empMapper.update(emp);
}
1
2
3
//EmpMapper
//更新员工
void update(Emp 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
<!--  更新员工  -->
<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="updateTime != null">
update_time = #{updateTime}
</if>
</set>
where id = #{id}
</update>

文件上传

简介

文件上传:将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。

前端页面三要素method="post"enctype="multipart/form-data"type="file"

1
2
3
4
5
6
<form action="/upload" method="post" enctype="multipart/form-data">
姓名: <input type="text" name="username"><br>
年龄: <input type="text" name="age"><br>
头像: <input type="file" name="image"><br>
<input type="submit" value="提交">
</form>

enctype的默认值是application/ x-www-form-urlencoded,若使用该方式上传文件,则最后上传的请求为:username=&age=&image=%E4%B8%AD%E5%9B%BD%E6%A2%A6.txt。即:

1
2
3
4
5
{
"username": "",
"age": "",
"image": "中国梦.txt"
}

指定enctype的值为multipart/form-data,使用该方式上传文件,在火狐浏览器(其他浏览器已经封装好文件,无法查看文件具体内容)中打开可以看到上传的是整个文件。其中boundary分割符

将前端页面upload.html放在src/main/resources/static文件夹下,在src/main/java/com/itheima/controller下编写UploadController.java文件,用来设置上传文件。

1
2
3
4
5
6
7
8
9
10
11
package com.itheima.controller;

@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload") //和前端的action字段一致
public Result upload(String username, Integer age, MultipartFile image) {
log.info("文件上次:{},{},{}", username, age, image);
return Result.success();
}
}

【注意】

1.服务端使用MultipartFile来接收文件。

2.在log.info处设置断点,浏览器使用http://localhost:8080/upload.html访问前端页面并提交文件,在IDEA中可以收到MultipartFile的文件信息。

其中,上述C:\Users\srr18\AppData\Local\Temp\tomcat.8080.10055437552755735705\work\Tomcat\localhost\ROOT存储的就是上传的临时文件,0.tmpusername的值surourou1.tmpage的值182.tmp是上传的文件。

当该函数运行结束,则该临时文件就会自动删除。

本地存储

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

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.itheima.controller;

@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload") //和前端的action字段一致
public Result upload(String username, Integer age, MultipartFile image) throws Exception {
log.info("文件上次:{},{},{}", username, age, image);

//获取原始文件名
String originalFilename = image.getOriginalFilename();

//构造唯一的文件名(不能重复)--uuid(通用唯一识别码)
int index = originalFilename.lastIndexOf(".");
String extname = originalFilename.substring(index);//获取文件后缀
String newFileName = UUID.randomUUID().toString() + extname;
log.info("新的文件名:{}", newFileName);

//将文件存储在服务器的磁盘目录中:我存在了resources的static目录下的images文件夹中
image.transferTo(new File("E:/2_学习/JavaWeb/code/itheima_web_project/springboot-web-tlias/src/main/resources/static/images/"+ newFileName));

return Result.success();
}
}

SpringBoot中,文件上传默认单个文件允许最大大小为1M。如果上传的文件大于1M,则会报错:

使用Postman上传大于1M的文件,会报413 Request Entity Too Large Request is too large for the server to process.,显示无法上传文件。

如果需要上传大文件,可以进行如下配置:

1
2
3
4
#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB

MultipartFile类的方法:

1
2
3
4
5
String getOriginalFilename(); //获取原始文件名
void transferTo(File dest); //将接收的文件转存到磁盘文件中
long getSize(); //获取文件的大小,单位:字节
byte[] getBytes(); //获取文件内容的字节数组
InputStream getInputStream(); //获取接收到的文件内容的输入流
阿里云OSS

阿里云对象存储OSSObject Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

阿里云使用步骤

Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。

SDKSoftware Development Kit的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK

1.手机注册登录阿里云账号。

2.在右上角点击控制台

3.进入控制台页面,点击左上角阿里云logo,搜索oss,点击对象存储服务。

4.第一次进入可能显示需要充值,可以认证之后点击免费试用,申请免费试用可以试用三个月。

5.创建Bucket列表。

6.创建Bucket成功后在概览可以查看Endpoint

7.在文件列表可以查看上传的文件。

8.在个人账户头像处选择AccessKey管理,选择创建AccessKey,并获取保存AccessKey IDAccessKey Secret(只能查看一次,需要保存下来)。

9.列表左下角找到SDK,点击Java文档可以查看帮助文档,文档中心打开查看。

image-20241023102108191

10.SDK参考->Java->安装下找到安装SDK,在pom.xml文件中引入如下依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--阿里云OSS依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>

11.SDK参考->Java->对象/文件->上传文件->简单上传,找到上传文件流,复制这段代码到test下进行测试(这段代码导入还需要导入一些包,按照IDEA提示导入即可),即springboot-web-tlias/src/test/java/com/itheima目录下的Demo.java文件,然后根据自己的信息修改即可。

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
package com.itheima;

import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;

public class Demo {

public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Bucket名称,例如examplebucket。
String bucketName = "srr-web-tlias";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "1.jpg";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "E:/2_学习/JavaWeb/code/itheima_web_project/springboot-web-tlias/src/main/resources/static/images/Space02-Default.jpg";
// 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
String region = "cn-hangzhou";

// 创建OSSClient实例。
ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialsProvider)
.clientConfiguration(clientBuilderConfiguration)
.region(region)
.build();

try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("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.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}

测试运行该段程序,即可将本地图片E:/2_学习/JavaWeb/code/itheima_web_project/springboot-web-tlias/src/main/resources/static/images/Space02-Default.jpg上传到阿里云的Bucket中,在文件列表就可以查看上传的文件。

【注意】上述代码需要在电脑本地配置环境变量,设置AccessKey IDAccessKey Secret的值。

image-20241023104712784

【补充】Windows使用CMD命令行设置环境变量

参考链接:Windows使用CMD命令行设置环境变量_windows cmd设置环境变量-CSDN博客

1.set设置临时环境变量。它的作用范围只限于当前窗口,在关闭该命令窗口后,所有通过set命令设置的变量都会丢失。

设置临时环境变量:set name=value

查看当前系统全部环境变量:set

2.setx设置永久性环境变量。这些变量不仅在当前命令行窗口中有效,而且在其他所有命令行窗口和应用程序中也都有效。

设置永久环境变量:setx name valuesetx /M name value

【报错】Exception in thread "main" com.aliyun.oss.common.auth.InvalidCredentialsException: Access key id should not be null or empty.

参考链接:03_阿里云_配置OSS环境变量_access key id should not be null or empty.-CSDN博客

1.执行以下命令配置RAM用户的访问密钥。

1
2
set OSS_ACCESS_KEY_ID=AccessKey ID
set OSS_ACCESS_KEY_SECRET=AccessKey Secret

2.执行以下命令以使更改生效。

1
2
setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID%"
setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET%"

3.执行以下命令验证环境变量配置。

1
2
echo %OSS_ACCESS_KEY_ID%
echo %OSS_ACCESS_KEY_SECRET%

配置环境变量后需要重启IDEA,这样IDEA就可以读取环境变量里的OSS_ACCESS_KEY_IDOSS_ACCESS_KEY_SECRET了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C:\Windows\System32>set OSS_ACCESS_KEY_ID=LTAI5tK5ZM5yGVVFWvKxeNqJ
C:\Windows\System32>set OSS_ACCESS_KEY_SECRET=
C:\Windows\System32>setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID%"

成功: 指定的值已得到保存。

C:\Windows\System32>setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET%"

成功: 指定的值已得到保存。

C:\Windows\System32>echo %OSS_ACCESS_KEY_ID%
LTAI5tK5ZM5yGVVFWvKxeNqJ

C:\Windows\System32>echo %OSS_ACCESS_KEY_SECRET%

阿里云集成

1.引入阿里云OSS上传文件工具类 AliOSSUtils(由官方的示例代码改造而来),放在com.itheima.utils包下。

【注意】需要修改endpointaccessKeyIdaccessKeySecretbucketName四个变量的值为自己bucket对应的值。其他的不用修改。

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.itheima.utils;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;

/**
* 阿里云 OSS 工具类
*/
@Component
public class AliOSSUtils {

private String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
private String accessKeyId = "LTAI5tK5ZM5yGVVFWvKxeNqJ";
private String accessKeySecret = "";
private String bucketName = "srr-web-tlias";

/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();

// 避免文件覆盖
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);

//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}

}

2.上传图片接口开发。

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

@Slf4j
@RestController
public class UploadController {
@Autowired
private AliOSSUtils aliOSSUtils;

@PostMapping("upload")
public Result upload(MultipartFile image) throws IOException {
log.info("文件上传,文件名:{}", image.getOriginalFilename());
//调用阿里云OSS工具类进行文件上传
String url = aliOSSUtils.upload(image);
log.info("文件上传完成,文件访问的url:{}", url);
return Result.success(url);
}
}

3.接口测试:在Postman中使用http://localhost:8080/upload接口,POST方法上传文件。

请求路径:/upload

请求方式:POST

接口描述:上传图片接口

配置文件

参数配置化

@Value注解通常用于外部配置的属性注入,具体用法为:@Value("${配置文件中的key}")

1
2
3
4
5
#阿里云OSS配置
aliyun.oss.endpoint=https://oss-cn-hangzhou.aliyuncs.com
aliyun.oss.accessKeyId=LTAI5tK5ZM5yGVVFWvKxeNqJ
aliyun.oss.accessKeySecret=
aliyun.oss.bucketName=srr-web-tlias
1
2
3
4
5
6
7
8
9
10
11
@Component
public class AliOSSUtils {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
}
yml配置文件
配置格式

SpringBoot提供了多种属性配置方式。

application.properties

1
2
server.port=8080
server.address=127.0.0.1

application.yml

1
2
3
server:
port: 8080
address: 127.0.0.1

application.yaml

1
2
3
server:
port: 8080
address: 127.0.0.1

yml基本语法
  1. 大小写敏感。
  2. 数值前边必须有空格,作为分隔符。
  3. 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(IDEA中会自动将Tab转换为空格)。
  4. 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可。
  5. #表示注释,从这个字符一直到行尾,都会被解析器忽略。
yml数据格式

对象/Map集合:

1
2
3
4
user:
name: zhangsan
age: 18
password: 123456

数组/List/Set集合:

1
2
3
4
hobby:
- java
- game
- sport
修改application.properties文件

application.yml中的配置案例相关的配置项:(将application.properties修改为application.yml

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
server:
port: 8080 #内嵌Tomcat的端口号

spring:
application:
name: springboot-web-tlias
#数据库连接信息
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/tlias
username: root
password:

#文件上传的配置
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

#阿里云OSS
aliyun:
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com
accessKeyId: LTAI5tK5ZM5yGVVFWvKxeNqJ
accessKeySecret:
bucketName: srr-web-tlias
@ConfigurationProperties

@ConfigurationProperties@Value都是用来注入外部配置的属性的。

不同:

@Value注解只能一个一个的进行外部属性的注入。

@ConfigurationProperties可以批量的将外部的属性配置注入到bean对象的属性中。

1
2
3
4
5
6
7
8
9
10
11
package com.itheima.utils;

@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
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
package com.itheima.utils;

/**
* 阿里云 OSS 工具类
*/
@Component
public class AliOSSUtils {
@Autowired
private AliOSSProperties aliOSSProperties;

/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws IOException {
//获取阿里云OSS参数
String endpoint = aliOSSProperties.getEndpoint();
String accessKeyId = aliOSSProperties.getAccessKeyId();
String accessKeySecret = aliOSSProperties.getAccessKeySecret();
String bucketName = aliOSSProperties.getBucketName();

// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();

// 避免文件覆盖
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);

//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}

pom.xml文件引入依赖(可选,引入该依赖,在配置yml时会有提示):

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

登录认证

员工登录

请求路径:/login

请求方式:POST

接口描述:该接口用于员工登录Tlias智能学习辅助系统,登录完毕后,系统下发JWT令牌。

请求参数样例:

1
2
3
4
{
"username": "jinyong",
"password": "123456"
}

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

@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;

@PostMapping("login")
public Result login(@RequestBody Emp emp) {
log.info("员工登录:{}", emp);
Emp e = empService.login(emp);
return e != null ? Result.success() : Result.error("用户名或密码错误");
}
}
1
2
3
//EmpService
//员工登录
Emp login(Emp emp);
1
2
3
4
5
6
//EmpServiceImpl
//员工登录
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
1
2
3
//根据用户名和密码查询员工
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);

登录校验

会话技术

会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。

会话跟踪方案:

  • 客户端会话跟踪技术:Cookie
  • 服务端会话跟踪技术:Session
  • 令牌技术

优点:HTTP协议中支持的技术。

缺点:移动端APP无法使用Cookie,不安全,用户可以自己禁用CookieCookie不能跨域。(跨域区分三个维度:协议、IP/域名、端口,任何一个不同则是跨域。)

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

import com.itheima.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
/**
* HttpSession演示
*/
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}

//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}

【报错】

参考链接:请求500失败-No primary or single unique constructor found-记录SpringBoot3.0 做导出功能HttpServletResponse的导包问题_no primary or single unique constructor found for -CSDN博客

pom.xml文件下导入会话技术依赖(错误,不需要引入)。但是使用javax.servlet-api会报错java.lang.IllegalStateException: No primary or single unique constructor found for interface javax.servlet.http.HttpServletResponse

1
2
3
4
5
6
<!--会话技术-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>

解决:这是因为springboot3.0的导出的依赖要从import javax.servlet.http.HttpServletResponse; 变为import jakarta.servlet.http.HttpServletResponse;

原来使用会报错:

1
2
3
4
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

修改为即可正常运行:

1
2
3
4
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

访问http://localhost:8080/c1,服务端会设置Cookie,在响应标头会显示set-cookie

浏览器识别到set-cookie会将Cookie的值存储到浏览器本地。在Application应用程序中的Cookie下可以查看Cookie的值。

以后的每次请求浏览器都会将Cookie携带到服务端,访问http://localhost:8080/c2,浏览器将在请求标头的Cookie字段中将Cookie的值携带到服务端。

Session

优点:存储在服务端,安全。

缺点:服务器集群环境下无法直接使用SessionCookie的缺点。

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.itheima.controller;

/**
* HttpSession演示
*/
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());

session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}

@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());

Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}

访问http://localhost:8080/s1,在响应标头set-cookie下的JSESSIONID代表服务器端session会话对象的ID

浏览器会将Cookie的值存储到浏览器本地。在Application应用程序中的Cookie下可以查看JSESSIONID的值。

访问http://localhost:8080/s2,浏览器将在请求标头的Cookie字段中将Cookie的值,上述设置的login_usernameJSESSIONID这两个Cookie都携带到服务端。服务器端会根据JSESSIONID找到对应的会话对象。

令牌技术

优点:支持PC端、移动端,解决集群环境下的认证问题,减轻服务器端存储压力。

缺点:需要自己实现。

【补充:cookiesessionJWT的区别】

参考链接:

还分不清 Cookie、Session、Token、JWT?看这一篇就够了-阿里云开发者社区 (aliyun.com)

一文彻底搞清session、cookie、token的区别 - 知乎 (zhihu.com)

面试必问:session,cookie和token的区别-腾讯云开发者社区-腾讯云 (tencent.com)

跨域:从头到尾讲讲 cookie?同源策略?跨越?解决跨域问题?_同源策略cookie-CSDN博客

JWT令牌
JWT

JWT全称:JSON Web Tokenhttps://jwt.io/ )。定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

组成:

  1. 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}
  2. 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
  3. 第三部分:Signature(签名),防止Token被篡改、确保安全性。将headerpayload,并加入指定秘钥,通过指定签名算法计算而来。

第一部分和第二部分使用Base64编码,可以通过Base64解码得到。

场景:登录认证

  • 登录成功后,生成令牌。
  • 后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理。
JWT生成和解析

1.引入依赖。老师用的是0.9.1版本的,我用的是最新版本的0.12.3

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.3</version>
</dependency>

2.生成JWT和解析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
package com.itheima;

//@SpringBootTest
//因为没使用到SpringBoot的功能,可以注释掉@SpringBootTest,加快测试的速度,否则需要加载整个SpringBoot工程会很慢
class SpringbootWebTliasApplicationTests {//在测试主方法下进行测试
//生成JWT
@Test
public void testGenJwt(){
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("name", "surourou");

String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "itheimaitheimaitheimaitheimaitheimaitheimaitheimaitheima") //签名算法:0.12.3版本要求签名算法必须大于或等于256bit,0.9.1版本没有要求
.setClaims(claims) //自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //设置有效期为1h
.compact(); //将结果包装为字符串
System.out.println(jwt);
}

//解析JWT
@Test
public void testParseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("itheimaitheimaitheimaitheimaitheimaitheimaitheimaitheima") //指定签名密钥
.build() //0.12.3版本需要使用build()方法,0.9.1版本可以省略这一个方法
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoic3Vyb3Vyb3UiLCJpZCI6MSwiZXhwIjoxNzI5NzM2Njc0fQ.kOLJv1uPsmn-bEUagHO53L1esUl3c7f_4JlbMUHKso0") //解析令牌
.getBody();
System.out.println(claims);
}

}

注意:

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

报错:

  1. io:签名秘钥需要大于或等于256bit
  2. io.jsonwebtoken.MalformedJwtException: Malformed protected header JSON: Unable to deserialize: Illegal character ((CTRL-CHAR, code 19)): only regular white space (\r, \n, \t) is allowed between tokens at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION disabled); line: 1, column: 2]Jwt令牌错误。
  3. io.jsonwebtoken.ExpiredJwtException: JWT expired 23961 milliseconds ago at 2024-10-24T01:32:32.000Z. Current time: 2024-10-24T01:32:55.961Z. Allowed clock skew: 0 milliseconds.Jwt令牌过期异常。
登录生成令牌

1.引入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
33
34
package com.itheima.utils;

public class JwtUtils {
private static String signKey = "itheimaitheimaitheimaitheimaitheimaitheimaitheimaitheima";
private static Long expire = 43200000L;

/**
* 生成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)
.build()
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}

2.登录完成后,调用工具类生成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
package com.itheima.controller;

@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;

@PostMapping("login")
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("name", e.getName());
claims.put("username", e.getUsername());

String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}

//登录失败,返回错误信息
return Result.error("用户名或密码错误");
}
}

进入登录页面,输入账号名和密码点击登录后,控制台Network网络会返回响应数据,响应数据包含生成的JWT令牌

前端会将JWT令牌存储在本地,即存储在Application应用程序下的Local Storage本地存储中。本地存储在移动端也支持,而Cookie只支持浏览器,在移动端不支持。(这是通过前端来存储的)

前端将该JWT令牌的值存储起来,在后续的每一次请求当中都会将JWT令牌获取出来,然后携带到服务端。如:请求dept查询部门数据,前端在请求头token中携带了JWT令牌的值。

使用Postman测试:

请求路径:/login

请求方式:POST

接口描述:该接口用于员工登录Tlias智能学习辅助系统,登录完毕后,系统下发JWT令牌。

请求参数样例:

1
2
3
4
{
"username": "jinyong",
"password": "123456"
}
过滤器Filter

概念:Filter过滤器,是JavaWeb三大组件(ServletFilterListener)之一。

过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。

过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。

快速入门

1.定义Filter:定义一个类,实现Filter接口,并重写其所有方法。

注意:必须使用jakarta下的包,不能使用javaxjavax包虽然不报错,但是无法实现过滤功能。

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.itheima.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
//import javax.servlet.*;
//import javax.servlet.annotation.WebFilter;
//注意:必须使用jakarta下的包,不能使用javax,javax包虽然不报错,但是无法实现过滤功能。

import java.io.IOException;

@WebFilter(urlPatterns = "/*")
public class DemoFilter 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("拦截到了请求");
//放行
filterChain.doFilter(servletRequest, servletResponse);
}

@Override //销毁方法,只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}

2.配置FilterFilter类上加@WebFilter注解,配置拦截资源的路径。引导类上加@ServletComponentScan开启Servlet组件支持。

FilterJavaWeb三大组件之一,并不是SpringBoot当中提供的功能,在SpringBoot要想使用JavaWeb三大组件,必须要使用@ServletComponentScan组件,表示开启了对servlet组件的支持。

1
2
3
4
5
6
7
8
9
package com.itheima;

@ServletComponentScan //开启对servlet组件的支持
@SpringBootApplication
public class SpringbootWebTliasApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebTliasApplication.class, args);
}
}
详解(执行流程、拦截路径、过滤器链)

1.执行流程:请求 –> 放行前逻辑 –> 放行 –> 资源 –> 放行后逻辑。

放行后访问对应资源,资源访问完成后,还会回到Filter中。回到Filter中是执行放行后的逻辑。

2.拦截路径

Filter可以根据需求,配置不同的拦截资源路径:Filter类上加@WebFilter注解,配置拦截资源的路径。

拦截路径 urlPatterns值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截

3.过滤器链:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。

顺序:注解配置的Filter优先级是按照过滤器类名(字符串)的自然排序

doFilter中的参数FilterChain filterChain就是一个过滤器链,执行filterChain.doFilter就是放行到下一个过滤器,如果当前是最后一个过滤器,就会放行到Web资源当中来访问Web资源。

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

@WebFilter(urlPatterns = "/*")
public class AbcFilter implements Filter {
@Override //初始化方法,只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Abc init 初始化方法执行了");
}

@Override //拦截到请求之后调用,调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Abc 拦截到了请求,放行前逻辑");
//放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("Abc 拦截到了请求,放行后逻辑");
}

@Override //销毁方法,只调用一次
public void destroy() {
System.out.println("Abc destroy 销毁方法执行了");
}
}

执行顺序:(注意:初始化和销毁的顺序不确定)

1
2
3
4
Abc 拦截到了请求,放行前逻辑
Demo 拦截到了请求,放行前逻辑
Demo 拦截到了请求,放行后逻辑
Abc 拦截到了请求,放行后逻辑

如果类名修改为AbcFilter,则执行顺序为:

1
2
3
4
Demo 拦截到了请求,放行前逻辑
Xbc 拦截到了请求,放行前逻辑
Xbc 拦截到了请求,放行后逻辑
Demo 拦截到了请求,放行后逻辑
登录校验Filter

流程:

  1. 获取请求url
  2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
  3. 获取请求头中的令牌(token)。
  4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
  5. 解析token,如果解析失败,返回错误结果(未登录)。
  6. 放行。

pom.xml文件中引入依赖:

1
2
3
4
5
6
<!--阿里巴巴fastJSON-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.52</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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package com.itheima.filter;

import com.alibaba.fastjson.JSONObject;
import com.itheima.pojo.Result;
import com.itheima.utils.JwtUtils;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
//必须使用jakarta下的包,使用javax会报错

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.IOException;

@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//强转
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;

//1.获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}", url);

//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("login")){
log.info("登陆操作,放行");
filterChain.doFilter(servletRequest, servletResponse);
return;
}

//3.获取请求头中的令牌(token)
String jwt = req.getHeader("token");

//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(jwt)){//判断jwt令牌是否为null或者空串
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换,将对象转换为json数据响应 ---> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin); //输出流写入,响应给浏览器
return;
}

//5.解析token,如果解析失败,返回错误结果(未登录)
try{
JwtUtils.parseJWT(jwt);
}catch (Exception e){
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换,将对象转换为json数据响应 ---> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin); //输出流写入,响应给浏览器
return;
}

//6.放行
log.info("令牌合法,放行");
filterChain.doFilter(servletRequest, servletResponse);
}
}

使用前端页面测试时,需要在localStorage本地存储删除token值,然后随便点击一个页面,因为没有识别到token令牌,前端页面会跳转到登陆页面。登录成功后则会执行放行操作。

使用Postman测试时,随便一个申请(如:http://localhost:8080/depts)都会返回:`{"code":0,"msg":"NOT_LOGIN"}`。使用接口(http://localhost:8080/login)获取到token值:

1
2
3
4
5
{
"code": 1,
"msg": "success",
"data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTcyOTgwMzMwNH0.ePvji_UHQY8Yl88hoDntPt2FNZZ7Oc0duJzFpSNYAw0"
}

在访问其他接口时,在Headers处填写token值即可获取到访问信息。如果token值为空或者报错,都会返回{"code":0,"msg":"NOT_LOGIN"}

拦截器Interceptor

概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring`框架中提供的,用来动态拦截控制器方法的执行

作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

快速入门

1.定义拦截器,实现HandlerInterceptor接口,并重写其所有方法。

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

@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行,返回true:放行,返回false不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
return true;
}

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

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

2.注册拦截器。

1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.config;

@Configuration //配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override //注册拦截器
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
//addPathPatterns:需要拦截哪些资源,excludePathPatterns:不需要拦截哪些资源
}
}
详解(拦截路径、执行流程)

1.拦截路径:拦截器可以根据需求,配置不同的拦截路径:

拦截路径 含义 举例
/* 一级路径 能匹配/depts,/emps,/login,不能匹配 /depts/1
/** 任意级路径 能匹配/depts,/depts/1,/depts/1/2
/depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/** /depts下的任意级路径 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1

2.拦截器执行流程:

FilterInterceptor的区别:

接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。

拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

登录校验Interceptor

和登录校验Filter的流程、代码一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
package com.itheima.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.itheima.pojo.Result;
import com.itheima.utils.JwtUtils;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
////必须使用jakarta下的包,使用javax会报错

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行,返回true:放行,返回false不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//1.获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}", url);

//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("login")){
log.info("登陆操作,放行");
return true; //放行
}

//3.获取请求头中的令牌(token)
String jwt = req.getHeader("token");

//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(jwt)){//判断jwt令牌是否为null或者空串
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换,将对象转换为json数据响应 ---> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin); //输出流写入,响应给浏览器
return false;//不放行
}

//5.解析token,如果解析失败,返回错误结果(未登录)
try{
JwtUtils.parseJWT(jwt);
}catch (Exception e){
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换,将对象转换为json数据响应 ---> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin); //输出流写入,响应给浏览器
return false;//不放行
}

//6.放行
log.info("令牌合法,放行");
return true;
}

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

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

异常处理

出现异常时(比如:添加存在的部门时,会报错:java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '就业部' for key 'dept.name'),默认返回的结果不符合规范:

1
2
3
4
5
6
{
"timestamp": "2024-10-24T09:20:35.147+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/depts"
}

出现异常时,异常一层一层往上抛,最后抛给框架,框架返回上述JSON格式数据,里面装着错误信息。但是该错误信息不符合开发规范,需要返回Result格式的JSON数据。

出现异常时处理方法:

方案一:在Controller的方法中进行try…catch处理。(代码臃肿,不推荐)

方案二:全局异常处理器。(简单、优雅,推荐)

1
2
3
4
5
6
7
8
9
10
11
package com.itheima.exception;

//全局异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)//捕获所有异常
public Result exception(Exception e){
e.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}

事务管理

事务是一组操作的集合,它是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败。

  1. 开启事务(一组操作开始前,开启事务):start transaction / begin ;
  2. 提交事务(这组操作全部成功后,提交事务):commit ;
  3. 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
Spring事务管理

案例:解散部门:删除部门,同时删除该部门下的员工。

注解:@Transactional

位置:业务(service)层的方法上、类上、接口上。

作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务。

1
2
3
4
//EmpMapper
//根据部门ID删除该部门下的员工数据
@Delete("delete from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itheima.service.impl;

@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;

@Transactional
@Override
public void delete(Integer id) {
deptMapper.deleteById(id);//根据ID删除部门数据
//int i = 1/0;
empMapper.deleteByDeptId(id);//根据部门ID删除该部门下的员工
}
}

开启事务管理日志(可以显示事务开启、提交、回滚等信息):

1
2
3
4
#开启事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#事务管理日志

#创建日志
2024-10-25T14:22:20.343+08:00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080-exec-3] o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [com.itheima.service.impl.DeptServiceImpl.delete]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-10-25T14:22:20.346+08:00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080-exec-3] o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [HikariProxyConnection@1443413090 wrapping com.mysql.cj.jdbc.ConnectionImpl@6b466344] for JDBC transaction
2024-10-25T14:22:20.351+08:00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080-exec-3] o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [HikariProxyConnection@1443413090 wrapping com.mysql.cj.jdbc.ConnectionImpl@6b466344] to manual commit

#日志回滚
2024-10-25T14:22:20.368+08:00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080-exec-3] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction rollback
2024-10-25T14:22:20.369+08:00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080-exec-3] o.s.jdbc.support.JdbcTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@1443413090 wrapping com.mysql.cj.jdbc.ConnectionImpl@6b466344]
2024-10-25T14:22:20.392+08:00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080-exec-3] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1443413090 wrapping com.mysql.cj.jdbc.ConnectionImpl@6b466344] after transaction

#日志提交
2024-10-25T14:26:06.811+08:00 DEBUG 39052 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit
2024-10-25T14:26:06.812+08:00 DEBUG 39052 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@2140932661 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cd75022]
2024-10-25T14:26:06.826+08:00 DEBUG 39052 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@2140932661 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cd75022] after transaction
事务属性
回滚

默认情况下,只有出现RuntimeException才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务。

1
2
3
4
5
6
7
8
9
10
@Transactional(rollbackFor = Exception.class) //出现所有的异常事务都会回滚
@Override
public void delete(Integer id) throws Exception {//在DeptService和DeptController的delete方法都要throws Exception
deptMapper.deleteById(id);//根据ID删除部门数据
//int i = 1/0; //运行时异常:ArithmeticException extends RuntimeException
if(true){
throw new Exception("出错啦");//不是运行时异常,不会进行rollback回滚操作
}
empMapper.deleteByDeptId(id);//根据部门ID删除该部门下的员工
}
传播行为

事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

属性值 含义
REQUIRED 【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
SUPPORTS 支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY 必须有事务,否则抛异常
NEVER 必须没事务,否则抛异常
案例

需求:解散部门时,无论是成功还是失败,都要记录操作日志。

步骤:

  1. 解散部门:删除部门、删除部门下的员工。
  2. 记录日志到数据库表中。

REQUIRED:大部分情况下都是用该传播行为即可。

REQUIRES_NEW:不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。

1.新建部门操作日志表。

1
2
3
4
5
create table dept_log(
id int auto_increment comment '主键ID' primary key,
create_time datetime null comment '操作时间',
description varchar(300) null comment '操作描述'
)comment '部门操作日志表';

2.记录日志到数据库表中。

1
2
3
4
5
6
7
8
9
10
package com.itheima.pojo;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {//日志类
private Integer id;
private LocalDateTime createTime;
private String description;
}
1
2
3
4
5
package com.itheima.service;

public interface DeptLogService {
void insert(DeptLog deptLog);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.itheima.service.impl;

@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;

@Transactional//(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
1
2
3
4
5
6
7
package com.itheima.mapper;

@Mapper
public interface DeptLogMapper {
@Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
void insert(DeptLog 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
package com.itheima.service.impl;

@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;

@Transactional(rollbackFor = Exception.class) //出现所有的异常事务都会回滚
@Override
public void delete(Integer id) throws Exception {
try {
deptMapper.deleteById(id);//根据ID删除部门数据

int i = 1/0; //运行时异常:ArithmeticException extends RuntimeException

empMapper.deleteByDeptId(id);//根据部门ID删除该部门下的员工
} finally {//无论是成功还是失败,都要记录操作日志,所以需要放到finally代码中
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次操作的是:" + id + "号部门");
deptLogService.insert(deptLog);
}
}
}

当删除某一个部门时,运行结果:

1
2
3
4
5
6
7
8
9
# 操作日志
2024-10-27T10:51:48.305+08:00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [com.itheima.service.impl.DeptServiceImpl.delete]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
2024-10-27T10:51:48.567+08:00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [HikariProxyConnection@504771362 wrapping com.mysql.cj.jdbc.ConnectionImpl@7d17447b] for JDBC transaction
2024-10-27T10:51:48.569+08:00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [HikariProxyConnection@504771362 wrapping com.mysql.cj.jdbc.ConnectionImpl@7d17447b] to manual commit
2024-10-27T10:51:48.618+08:00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Participating in existing transaction
2024-10-27T10:51:48.627+08:00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction rollback
2024-10-27T10:51:48.627+08:00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@504771362 wrapping com.mysql.cj.jdbc.ConnectionImpl@7d17447b]
2024-10-27T10:51:48.629+08:00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@504771362 wrapping com.mysql.cj.jdbc.ConnectionImpl@7d17447b] after transaction
java.lang.ArithmeticException: / by zero

执行过程:默认的事务传播行为是REQUIRED,如果public void insert(DeptLog deptLog)方法使用默认的传播行为,调用deptLogService.insert(deptLog)方法时,调用已存在的事务empMapper.deleteByDeptId(id),即:执行empMapper.deleteByDeptId(id)deptLogService.insert(deptLog)两个方法共用一个事务。执行deptLogService.insert(deptLog)成功插入一条语句后,由于在该方法执行过程中抛出了java.lang.ArithmeticException: / by zero的算术运算异常,所以整个事务都需要回滚Initiating transaction rollback,由于两个方法共用一个事务,所以两个方法都会回滚,所以无法插入操作日志。

解决方法:设置public void insert(DeptLog deptLog)的传播行为为REQUIRES_NEW。无论当前运行方法中是否有事务,在调用public void insert(DeptLog deptLog)方法时,都会开启一个新的事务。

1
2
3
4
5
6
//DeptLogServiceImpl类
@Transactional(propagation = Propagation.REQUIRES_NEW)//修改事务传播行为为REQUIRES_NEW即可
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}

当删除某一个部门时,运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#操作日志
2024-10-27T10:59:46.505+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [com.itheima.service.impl.DeptServiceImpl.delete]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
2024-10-27T10:59:46.755+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [HikariProxyConnection@1598400954 wrapping com.mysql.cj.jdbc.ConnectionImpl@73cb1ed4] for JDBC transaction
2024-10-27T10:59:46.757+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [HikariProxyConnection@1598400954 wrapping com.mysql.cj.jdbc.ConnectionImpl@73cb1ed4] to manual commit
2024-10-27T10:59:46.813+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Suspending current transaction, creating new transaction with name [com.itheima.service.impl.DeptLogServiceImpl.insert]
2024-10-27T10:59:46.833+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [HikariProxyConnection@1499621420 wrapping com.mysql.cj.jdbc.ConnectionImpl@79341019] for JDBC transaction
2024-10-27T10:59:46.833+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [HikariProxyConnection@1499621420 wrapping com.mysql.cj.jdbc.ConnectionImpl@79341019] to manual commit
2024-10-27T10:59:46.844+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit
2024-10-27T10:59:46.844+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@1499621420 wrapping com.mysql.cj.jdbc.ConnectionImpl@79341019]
2024-10-27T10:59:46.847+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1499621420 wrapping com.mysql.cj.jdbc.ConnectionImpl@79341019] after transaction
2024-10-27T10:59:46.849+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Resuming suspended transaction after completion of inner transaction
2024-10-27T10:59:46.849+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction rollback
2024-10-27T10:59:46.850+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@1598400954 wrapping com.mysql.cj.jdbc.ConnectionImpl@73cb1ed4]
2024-10-27T10:59:46.853+08:00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080-exec-1] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1598400954 wrapping com.mysql.cj.jdbc.ConnectionImpl@73cb1ed4] after transaction
java.lang.ArithmeticException: / by zero

执行过程:运行empMapper.deleteByDeptId(id)方法时会创建新事务,调用deptLogService.insert(deptLog)方法时,挂起当前事务,然后又创建一个新事务。当deptLogService.insert(deptLog)方法运行完毕后,提交该事务。当内部的事务运行结束之后,继续完成刚才挂起的事务,即empMapper.deleteByDeptId(id)方法所开启的事务。由于该事务运行过程中报错了,所以外部事务会进行正常的回滚操作。

AOP

AOP概述

AOPAspect Oriented Programming面向切面编程、面向方面编程),其实就是面向特定方法编程。

场景:案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时。

实现:动态代理是面向切面编程最主流的实现。而SpringAOPSpring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。

AOP快速入门

SpringAOP快速入门:统计各个业务层方法执行耗时】

1.导入依赖:在pom.xml中导入AOP的依赖。

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

2.编写AOP程序:针对于特定方法根据业务需要进行编程。

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

@Slf4j
@Component
@Aspect //AOP类
public class TimeAspect {
@Around("execution(* com.itheima.service.*.*(..))")//运行com.itheima.service包下所有的类或接口中的所有方法都会运行如下程序
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
//1.记录方法开始时间
long begin = System.currentTimeMillis();

//2.调用原始方法运行
Object result = joinPoint.proceed();

//3.记录结束时间,计算方法执行耗时
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature() + "方法执行耗时:{}ms", end - begin);

return result;
}
}

AOP场景:记录操作日志、权限控制、事务管理。

优势:代码无侵入、减少重复代码、提高开发效率、维护方便。

AOP核心概念

连接点JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)。

通知Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)。

切入点PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用。

切面Aspect,描述通知与切入点的对应关系(通知+切入点)。

目标对象Target,通知所应用的对象。

AOP执行流程(以下图为例):SpringAOP底层是基于动态代理技术实现的。在程序运行的时候,会基于动态代理技术为目标对象DeptServiceImpl生成一个代理对象DeptServiceProxy。在代理对象中会对目标对象的原始方法进行功能增强,增强的逻辑就是AOP里面定义的通知Advice):在方法运行开始之前先记录方法运行的开始时间,接下来调用原始的方法(list方法)执行,之后记录方法运行的结束时间,最后统计方法执行的耗时。最终在DeptController程序运行时,注入的的DeptService不再是目标对象DeptServiceImpl,而是注入代理对象DeptServiceProxy。最终调用list方法时,调用的是代理对象DeptServiceProxy中的list方法,这个list方法已经进行了功能的增强。

运行中断点显示:使用了Spring底层动态代理技术:CG类网动态代理。

通知类型
  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行。
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行。
  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行。
  • @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行。
  • @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行。

注意:

  1. @Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行。
  2. @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。
  3. @AfterReturning@AfterThrowing 是互斥的,只有一个会发生。
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
package com.itheima.aop;

@Slf4j
@Component
@Aspect
public class MyAspect {
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){
log.info("before ...");
}

@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");

//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();

log.info("around after ...");
return result;
}

@After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
//@Around("com.itheima.aop.MyAspect1.pt()")
public void after(){
log.info("after ...");
}

@AfterReturning("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void afterReturning(){
log.info("afterReturning ...");
}

@AfterThrowing("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void afterThrowing(){
log.info("afterThrowing ...");
}
}
1
2
3
4
5
6
#没有异常时的输出日志:没有afterThrowing
2024-10-27T15:45:38.714+08:00 INFO 14000 --- [springboot-web-tlias] [nio-8080-exec-1] com.itheima.aop.MyAspect : around before ...
2024-10-27T15:45:38.715+08:00 INFO 14000 --- [springboot-web-tlias] [nio-8080-exec-1] com.itheima.aop.MyAspect : before ...
2024-10-27T15:45:39.174+08:00 INFO 14000 --- [springboot-web-tlias] [nio-8080-exec-1] com.itheima.aop.MyAspect : afterReturning ...
2024-10-27T15:45:39.174+08:00 INFO 14000 --- [springboot-web-tlias] [nio-8080-exec-1] com.itheima.aop.MyAspect : after ...
2024-10-27T15:45:39.174+08:00 INFO 14000 --- [springboot-web-tlias] [nio-8080-exec-1] com.itheima.aop.MyAspect : around after ...
1
2
3
4
5
6
#在DeptServiceImpl类下的list方法加入int i = 1/0; 页面访问部门管理时会出现异常
#异常时的输出日志:没有afterReturning和around after
2024-10-27T15:53:17.835+08:00 INFO 2472 --- [springboot-web-tlias] [nio-8080-exec-1] com.itheima.aop.MyAspect : around before ...
2024-10-27T15:53:17.836+08:00 INFO 2472 --- [springboot-web-tlias] [nio-8080-exec-1] com.itheima.aop.MyAspect : before ...
2024-10-27T15:53:17.836+08:00 INFO 2472 --- [springboot-web-tlias] [nio-8080-exec-1] com.itheima.aop.MyAspect : afterThrowing ...
2024-10-27T15:53:17.836+08:00 INFO 2472 --- [springboot-web-tlias] [nio-8080-exec-1] com.itheima.aop.MyAspect : after ...
@PointCut

@PointCut:将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。

private:仅能在当前切面类中引用该表达式。

public:在其他外部的切面类中也可以引用该表达式。在当前切面类使用公共的切点表达式:@Around("pt()"),在其他外部切面类使用公共的切点表达式:@Around("com.itheima.aop.MyAspect1.pt()")

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

@Slf4j
@Component
@Aspect
public class MyAspect1 {
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pt(){}

@Before("pt()")
public void before(){
log.info("before ...");
}

@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");

//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();

log.info("around after ...");
return result;
}

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

@AfterReturning("pt()")
public void afterReturning(){
log.info("afterReturning ...");
}

@AfterThrowing("pt()")
public void afterThrowing(){
log.info("afterThrowing ...");
}
}
通知顺序

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。

1.不同切面类中,默认按照切面类的类名字母排序

  • 目标方法前的通知方法:字母排名靠前的先执行。
  • 目标方法后的通知方法:字母排名靠前的后执行。

2.用@Order(数字)加在切面类上来控制顺序

  • 目标方法前的通知方法:数字小的先执行。
  • 目标方法后的通知方法:数字小的后执行。
切入点表达式

切入点表达式:描述切入点方法的一种表达式。

作用:主要用来决定项目中的哪些方法需要加入通知

常见形式:

  • execution(……):根据方法的签名来匹配。
  • @annotation(……):根据注解匹配。
execution

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

1
execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)

其中带?的表示可以省略的部分。

  1. 访问修饰符:可省略(比如: publicprotected
  2. 包名.类名: 可省略
  3. throws异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

可以使用通配符描述切入点。

  • *单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分。

    1
    execution(* com.*.service.*.update*(*))
  • ..多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数。

    1
    execution(* com.itheima..DeptService.*(..))

注意:根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。

1
2
3
@Pointcut("execution(* com.itheima.service.DeptService.list()) || " +
"execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
private void pt(){}

书写建议:

  1. 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是find开头,更新类方法都是update开头。
  2. 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
  3. 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用..,使用*匹配单个包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.itheima.aop;

//切面类
@Slf4j
@Aspect
@Component
public class MyAspect6 {
//@Pointcut("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//@Pointcut("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")//省略访问权限修饰符
//@Pointcut("execution(void delete(java.lang.Integer))") //包名.类名不建议省略
//@Pointcut("execution(void com.itheima.service.DeptService.delete(java.lang.Integer))")//基于接口

//@Pointcut("execution(void com.itheima.service.DeptService.*(java.lang.Integer))")//DeptService下所有的方法,方法参数为Integer
//@Pointcut("execution(* com.*.service.DeptService.*(*))")//第二级包是任意的,DeptService下所有的方法,方法中必须有一个任意类型的参数
//@Pointcut("execution(* com.itheima.service.*Service.delete*(*))")//以Service结尾的类/接口下以delete开头的方法,方法中必须有一个参数

//@Pointcut("execution(* com.itheima.service.DeptService.*(..))")//DeptService下所有的方法,任意个任意类型的形参(没有也行)
//@Pointcut("execution(* com..DeptService.*(..))")//任意层级的包,DeptService下所有的方法,任意个任意类型的形参(没有也行)
//@Pointcut("execution(* com..*.*(..))")//第一级包是com,后面多少个层级无所谓,所有类中所有的方法
//@Pointcut("execution(* *(..))") //慎用,匹配当前环境中所有的方法

@Pointcut("execution(* com.itheima.service.DeptService.list()) || " +
"execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
private void pt(){}

@Before("pt()")
public void before(){
log.info("MyAspect6 ... before ...");
}
}
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
//在测试类进行测试
package com.itheima;

@SpringBootTest
class SpringbootAopQuickstart1ApplicationTests {
@Autowired
private DeptService deptService;

@Test
public void testAopDelete() throws Exception {
deptService.delete(10);
}

@Test
public void testAopList(){
List<Dept> list = deptService.list();
System.out.println(list);
}

@Test
public void testAopGetById(){
Dept dept = deptService.get(1);
System.out.println(dept);
}
}
@annotation

@annotation切入点表达式,用于匹配标识有特定注解的方法。

1
2
3
4
5
6
package com.itheima.aop;

@Retention(RetentionPolicy.RUNTIME)//运行时有效
@Target(ElementType.METHOD)//该注解可以作用在方法上
public @interface MyLog {//自定义注解标识方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.itheima.aop;

//切面类
@Slf4j
@Aspect
@Component
public class MyAspect7 {
//匹配DeptServiceImpl中的 list() 和 delete(Integer id)方法
//@Pointcut("execution(* com.itheima.service.DeptService.list()) || execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")

/*
要作用在com.itheima.service.DeptService.list()和com.itheima.service.DeptService.list()这两个方法上,
需要在这两个方法上加上@MyLog
*/
@Pointcut("@annotation(com.itheima.aop.MyLog)")
private void pt(){}

@Before("pt()")
public void before(){
log.info("MyAspect7 ... before ...");
}
}
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.itheima.service.impl;

@Service
public class DeptServiceImpl implements DeptService {
@MyLog
@Override
public List<Dept> list() {
//int i = 1/0;
return deptMapper.list();
}

@MyLog
@Transactional(rollbackFor = Exception.class) //出现所有的异常事务都会回滚
@Override
public void delete(Integer id) throws Exception {
try {
deptMapper.deleteById(id);//根据ID删除部门数据
//int i = 1/0; //运行时异常:ArithmeticException extends RuntimeException
//if(true){throw new Exception("出错啦");}//不是运行时异常,不会进行rollback回滚操作
empMapper.deleteByDeptId(id);//根据部门ID删除该部门下的员工
} finally {
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次操作的是:" + id + "号部门");
deptLogService.insert(deptLog);
}
}
}
连接点

Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

  1. 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint
  2. 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型。
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.itheima.aop;

//切面类
@Slf4j
@Aspect
@Component
public class MyAspect8 {

@Pointcut("execution(* com.itheima.service.DeptService.*(..))")
private void pt(){}

@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("MyAspect8 ... before ...");
}

@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("MyAspect8 around before ...");

//1. 获取目标对象的类名
String className = joinPoint.getTarget().getClass().getName();
log.info("目标对象的类名:{}", className);

//2. 获取目标方法的方法名
String methodName = joinPoint.getSignature().getName();
log.info("目标方法的方法名: {}",methodName);

//3. 获取目标方法运行时传入的参数
Object[] args = joinPoint.getArgs();
log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));

//4. 放行目标方法执行
//proceed有两个重载方法:有参(需要将上述获取到的参数args传入)和无参(调用无参就可以了)
Object result = joinPoint.proceed();

//5. 获取目标方法运行的返回值
log.info("目标方法运行的返回值: {}",result);

log.info("MyAspect8 around after ...");
return result;
}
}
AOP案例

【案例】将增、删、改相关接口的操作日志记录到数据库表中。日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。

1.导入资料中准备好的数据库表结构,并引入对应的实体类。

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
13
14
15
package com.itheima.pojo;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id; //ID
private Integer operateUser; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
1
2
3
4
5
6
7
8
9
package com.itheima.mapper;

@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}

2.自定义注解@Log

1
2
3
4
5
6
package com.itheima.anno;

@Retention(RetentionPolicy.RUNTIME)//运行时有效
@Target(ElementType.METHOD)//该注解可以作用在方法上
public @interface Log {//自定义注解标识方法
}

3.定义切面类,完成记录操作日志的逻辑。

获取当前登录用户:获取request对象,从请求头中获取到jwt令牌,解析令牌获取出当前用户的id

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

import jakarta.servlet.http.HttpServletRequest;//注意是jakarta(JDK17及以上),不是javax

@Slf4j
@Component
@Aspect//切面类
public class LogAspect {
@Autowired
private HttpServletRequest request;
//因为请求对象已经被spring自动存在了ioc容器中,所以这里可以直接使用@Autowired注入当前这次请求的请求对象

@Autowired
private OperateLogMapper operateLogMapper;

@Around("@annotation(com.itheima.anno.Log)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
//操作人ID:当前登录员工ID(从jwt令牌中获取)
//获取请求头中的jwt令牌,解析令牌
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operateUser = (Integer) claims.get("id");

//当前操作时间
LocalDateTime operateTime = LocalDateTime.now();

//操作类名
String className = joinPoint.getTarget().getClass().getName();

//操作方法名
String methodName = joinPoint.getSignature().getName();

//操作方法参数
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);

long begin = System.currentTimeMillis();
//调用原始目标方法运行
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();

//方法返回值
String returnValue = JSONObject.toJSONString(result);

//操作耗时
long costTime = end - begin;

//记录操作日志
OperateLog operateLog = new OperateLog(null, operateUser, operateTime, className, methodName, methodParams, returnValue, costTime);
operateLogMapper.insert(operateLog);

log.info("AOP记录操作日志:{}", operateLog);

return result;
}
}

4.在DeptControllerEmpController的增、删、改方法中加入@Log注解。


JavaWeb后端开发:SpringBoot案例
http://surourou8.github.io/2024/10/27/JavaWeb后端开发:SpringBoot案例/
作者
Su Rourou
发布于
2024年10月27日
许可协议