Java Web - 项目
准备工作
开发模式
当前主流开发模式: 前后端分离
前后端分离, 如何知道前后端定义的数据格式? 因此需要统一制定一套规范: 接口文档
RESTful
REST: 表述性状态转换, 一种软件架构风格
- 描述模块的功能通常使用复数, 也就是加 s 的格式来描述, 表示此类资源, 而非单个资源, 如: users, emps, books ...
Apifox
Apifox 是一款集成了 Api 文档、Api 调试、Api Mock、Api 测试的一体化协作平台
作用: 接口文档管理、接口请求测试、Mock服务
- 为什么要使用 Apifox: 浏览器地址栏发起的请求, 都是 GET 方式的请求, 我们需要发起 POST, PUT, DELETE 方式的请求需要借助工具
工程搭建
-
创建 SpringBoot 工程, 引入 web 开发起步依赖, mybatis, mysql 驱动, lombok
-
创建数据库 tlias, 创建数据库表 dept, 并在 application 中配置数据库基本信息
spring:application:name: tlias-web-managementdatasource:url: jdbc:mysql://localhost:3306/tliasdriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: xxxx# datasource:
# type: com.alibaba.druid.pool.DruidDataSource
# druid:
# url: jdbc:mysql://localhost:3306/tlias
# driver-class-name: com.mysql.cj.jdbc.Driver
# username: root
# password: xxxxmybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
- 准备基础代码结构, 并引入实体类 Dept 以及统一的响应结果封装类 Result
- 统一的响应结果封装类 Result
- Result.java
package com.example.tlias_web_management.pojo;import lombok.Data; import java.io.Serializable;/** * 后端统一返回结果 */ @Data public class Result {private Integer code; // 编码: 1 成功, 0 失败private String msg; // 信息private Object data; // 数据public static Result success() {Result result = new Result();result.code = 1;result.msg = "success";return result;}public static Result success(Object object) {Result result = new Result();result.code = 1;result.msg = "success";result.data = object;return result;}public static Result error(String msg) {Result result = new Result();result.code = 0;result.msg = msg;return result;}}
- 自动转成 json 格式
{"code": 1,"msg": "操作成功","data": ... }
{"code": 0,"msg": "密码错误","data": ... }
- 基本代码结构
- Dept.java
package com.example.tlias_web_management.pojo;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;import java.time.LocalDateTime;@Data @NoArgsConstructor @AllArgsConstructor public class Dept {private Integer id;private String name;private LocalDateTime createTime;private LocalDateTime updateTime; }
- 统一的响应结果封装类 Result
部门管理
列表查询
需求
- 由于部门数量较少, 不考虑分页显示
- 对查询的结果, 根据最后的修改时间倒序排序
- 接口文档
接口开发
三层架构:
-
Controller
- 接受请求
- 调用 Service 层
- 响应结果
package com.example.tlias_web_management.controller;import java.util.List;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;import com.example.tlias_web_management.pojo.Dept; import com.example.tlias_web_management.pojo.Result; import com.example.tlias_web_management.service.DeptService;@RestController public class DeptController {@Autowiredprivate DeptService deptService;@GetMapping("/depts") // 接口文档要求请求路径: /depts; 请求方式: GETpublic Result list() {System.out.println("查询全部部门数据");List<Dept> deptList = deptService.findAll();return Result.success(deptList);} }
-
Service
- 调用 Mapper 接口方法
package com.example.tlias_web_management.service.impl;import java.util.List;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;import com.example.tlias_web_management.mapper.DeptMapper; import com.example.tlias_web_management.pojo.Dept; import com.example.tlias_web_management.service.DeptService;@Service public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper;@Overridepublic List<Dept> findAll() {return deptMapper.findAll();} }
-
Mapper
- 执行 SQL 语句
SELECT * FROM dept ORDER BY update_time DESC;
package com.example.tlias_web_management.mapper;import java.util.List;import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select;import com.example.tlias_web_management.pojo.Dept;@Mapper public interface DeptMapper {/** 查询所有的部分数据*/@Select("SELECT * FROM dept ORDER BY update_time DESC")List<Dept> findAll();}
Apifox 测试
如图
结果封装
- 注意到 Apifox 测试结果中, createTime 和 updateTime 数据都是 null
-
原因
- 实体类属性名和数据库表查询返回的字段名一致, mybatis 会自动封装
- 不一致则不能自动封装
- 此处实体类属性名: createTime 和 updateTime; 数据库表字段: create_time 和 update_time
-
方案
- 手动结果映射
@Results({@Result(column = "create_time", property = "createTime")@Result(column = "update_time", property = "updateTime") }) @Select("SELECT * FROM dept ORDER BY update_time DESC") public List<Dept> findAll();
- SQL 语句中起别名, 字段的别名和属性名一致
@Select("SELECT id, name, create_time createTime, update_time updateTime FROM dept ORDER BY update_time DESC") public List<Dept> findAll();
- (推荐) 开启驼峰命名: 如果字段名符合下划线分割且属性名符合驼峰命名规则, mybatis 会自动通过驼峰命名规则映射
mybatis: configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 开启驼峰命名映射开关map-underscore-to-camel-case: true
-
前后端联调测试
解压资料中 nginx, 运行后端项目, 访问 localhost:90, 点击系统信息管理-部门管理, 成功查询到数据
但可以看到, 前端工程请求服务器的地址为 http://localhost:90/api/depts 并不是 localhost:8080/depts
- Nginx 反向代理
- 浏览器发起请求, 请求 localhost:90, 实际请求 nginx 服务器
- nginx 服务器中, 将请求转发给了后端的 tomcat 服务器, 由 tomcat 服务器处理请求
- 优势
- 安全: 后端服务器一般会搭建集群, 有多个服务器, 如果把所有 tomcat 暴露给前端, 让前端直接请求 tomcat, 对后端服务器比较危险
- 灵活: 后端服务器的变化对于前端是无感知的, 只需要在 nginx 中配置
- 负载均衡: nginx 反向代理可以方便的实现后端 tomcat 的负载均衡操作
删除部门
需求
- 接口文档
接口开发
三层架构:
- Controller
- 接收请求参数
/depts?id=8
(简单参数)- 方式一: HttpServletRequest
@DeleteMapping("/depts") public Result delete(HttpServletRequest request) {String idStr = request.getParameter("id");int id = Integer.parseInt(idStr);System.out.println("根据 ID 删除部门: " + id);return Result.success(); }
- 方式二:
@RequestParam
(默认 required = true 代表该参数必须传递, 改为 false 则不带参数时不会报错, 不带参数时参数默认 null)
@DeleteMapping("/depts") public Result delete(@RequestParam(value = "id", required = false) Integer deptId) {System.out.println("根据 ID 删除部门: " + deptId);return Result.success(); }
- (推荐) 方法三: 如果请求参数名和形参变量名一致, 直接定义方法形参即可接收
@DeleteMapping("/depts") public Result delete(Integer id) {System.out.println("根据 ID 删除部门: " + id);return Result.success(); }
- 调用 Service 层
- 响应结果
@DeleteMapping("/depts") public Result delete(Integer id) {System.out.println("根据 ID 删除部门: " + id);deptService.deleteById(id);return Result.success(); }
- 接收请求参数
- Service
- 调用 Mapper 接口方法
@Override public void deleteById(Integer id) {deptMapper.deleteById(id); }
- Mapper
- SQL
delete from dept where id = ?
@Delete("DELETE FROM dept WHERE id = #{id}") void deleteById(Integer id);
Apifox 测试
成功
前后端联调测试
失败
新增部门
需求
- 接口文档
接口开发
三层架构:
-
Controller
-
接收请求参数
/depts
, JSON 格式的参数:{"name": "行政部"}
- JSON 格式的参数, 通常会使用一个实体对象进行接收
- JSON 数据的键名与方法形参对象的属性名相同, 并需要使用
@RequestBody
注解标识 (此处 "name" 与实体类 Dept 中 name 属性名相同)
-
调用 Service 层
-
响应结果
@PostMapping("/depts") public Result add(@RequestBody Dept dept) {System.out.println("新增部门: " + dept);deptService.add(dept);return Result.success(); }
-
-
Service
- 补全基本属性
- 调用 Mapper 接口方法
@Override public void add(Dept dept) {// 补全基础属性 createTime, updateTimedept.setCreateTime(LocalDateTime.now());dept.setUpdateTime(LocalDateTime.now());// 调用 Mapper 接口方法deptMapper.insert(dept); }
-
Mapper
- SQL
insert into dept (name, create_time, update_time) values (?, ?, ?)
@Insert("INSERT INTO dept(name, create_time, update_time) values(#{name}, #{createTime}, #{updateTime})") void insert(Dept dept);
Apifox 测试
- 使用 POST, JSON 格式参数在请求体 Body 中
前后端联调测试
- 新增财务部
修改部门
查询回显
需求
- 接口文档
接口开发
三层架构:
-
Controller
- 接口请求参数 (路径参数)
/depts/1
- 路径参数: 通过请求 URL 直接传递参数, 使用 {...} 来标识该路径参数, 使用
@PathVariable
获取
@GetMapping("/depts/{id}") public Result getInfo(@PathVariable("id") Integer deptId) {System.out.println("根据 ID 查询部门: " + deptId);return Result.success(); }
- 如果路径参数名称和形参名称一致, 可以省略注解
@GetMapping("/depts/{id}") public Result getInfo(@PathVariable Integer id) {System.out.println("根据 ID 查询部门: " + id);return Result.success(); }
- 路径参数: 通过请求 URL 直接传递参数, 使用 {...} 来标识该路径参数, 使用
- 调用 Service 层
- 响应结果
@GetMapping("/depts/{id}") public Result getInfo(@PathVariable Integer id) {System.out.println("根据 ID 查询部门: " + id);Dept dept = deptService.getById(id);return Result.success(dept); }
- 接口请求参数 (路径参数)
-
Service
- 调用 Mapper 接口方法
@Override public Dept getById(Integer id) {return deptMapper.getById(id); }
-
Mapper
- SQL
select * from dept where id = ?
@Select("SELECT * FROM dept where id = #{id}") Dept getById(Integer id);
Apifox 测试
修改数据
需求
- 接口文档
接口开发
三层架构:
-
Controller
- 接收请求参数
/depts
, JSON 格式的参数:{"id": 1, "name": "行政部"}
- 调用 Service 层
- 响应结果
@PutMapping("/depts") public Result update(@RequestBody Dept dept) {System.out.println("修改部门: " + dept);deptService.update(dept);return Result.success(); }
- 接收请求参数
-
Service
- 补全基础属性
- 调用 Mapper 接口方法
@Override public void update(Dept dept) {// 补全基础属性 updateTimedept.setUpdateTime((LocalDateTime.now()));// 调用 Mapper 接口方法deptMapper.update(dept); }
-
Mapper
- SQL (需要更新 name 和 update_time)
update dept set name = ?, update_time = ? where id = ?
@Update("UPDATE dept SET name = #{name}, update_time = #{updateTime} where id = #{id}") void update(Dept dept);
测试
Apifox 与前后端联调测试均成功
@RequestMapping 抽取公共路径
在 DeptController 中 @RequestMapping
中路径均是 /depts
@RestController
public class DeptController {@Autowiredprivate DeptService deptService;/** 查询部门列表*/@GetMapping("/depts") // 接口文档要求请求路径: /depts; 请求方式: GETpublic Result list() {System.out.println("查询全部部门数据");List<Dept> deptList = deptService.findAll();return Result.success(deptList);}/** 根据 ID 删除部门*/@DeleteMapping("/depts")public Result delete(Integer id) {System.out.println("根据 ID 删除部门: " + id);deptService.deleteById(id);return Result.success();}/** 新增部门*/@PostMapping("/depts")public Result add(@RequestBody Dept dept) {System.out.println("新增部门: " + dept);deptService.add(dept);return Result.success();}/** 根据 ID 查询部门*/@GetMapping("/depts/{id}")public Result getInfo(@PathVariable Integer id) {System.out.println("根据 ID 查询部门: " + id);Dept dept = deptService.getById(id);return Result.success(dept);}/** 根据 ID 修改部门*/@PutMapping("/depts")public Result update(@RequestBody Dept dept) {System.out.println("修改部门: " + dept);deptService.update(dept);return Result.success();}
}
可以把公共路径抽取出来, 注解在类上
@RequestMapping("/depts")
@RestController
public class DeptController {@Autowiredprivate DeptService deptService;/** 查询部门列表*/@GetMapping // 接口文档要求请求路径: /depts; 请求方式: GETpublic Result list() {System.out.println("查询全部部门数据");List<Dept> deptList = deptService.findAll();return Result.success(deptList);}/** 根据 ID 删除部门*/@DeleteMappingpublic Result delete(Integer id) {System.out.println("根据 ID 删除部门: " + id);deptService.deleteById(id);return Result.success();}/** 新增部门*/@PostMappingpublic Result add(@RequestBody Dept dept) {System.out.println("新增部门: " + dept);deptService.add(dept);return Result.success();}/** 根据 ID 查询部门*/@GetMapping("/{id}")public Result getInfo(@PathVariable Integer id) {System.out.println("根据 ID 查询部门: " + id);Dept dept = deptService.getById(id);return Result.success(dept);}/** 根据 ID 修改部门*/@PutMappingpublic Result update(@RequestBody Dept dept) {System.out.println("修改部门: " + dept);deptService.update(dept);return Result.success();}
}
日志技术
sout 只能输出到控制台, 不便于扩展, 维护, 因此要使用日志
Logback 入门
- 引入配置文件 logback.xml 在 resource 下
<?xml version="1.0" encoding="UTF-8"?>
<configuration><!-- 控制台输出 --><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符 --><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern></encoder></appender><!-- 日志输出级别 --><root level="ALL"><appender-ref ref="STDOUT" /></root>
</configuration>
- 记录日志: 定义日志记录对象 Logger, 记录日志
package com.example.tlias_web_management;import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class LogTest {//定义日志记录对象private static final Logger log = LoggerFactory.getLogger(LogTest.class);@Testpublic void testLog(){log.debug("开始计算...");int sum = 0;int[] nums = {1, 5, 3, 2, 1, 4, 5, 4, 6, 7, 4, 34, 2, 23};for (int num : nums) {sum += num;}log.info("计算结果为: " + sum);log.debug("结束计算...");}}
Logback 配置文件
可以配置输出的格式, 位置以及日志开关等
常见两种输出日志的位置:
- 控制台
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">...</appender>
- 系统文件
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">...</appender>
日志开关: 开启 ALL, 关闭 OFF
<root level="ALL"><appender-ref ref="STDOUT" />
</root>
- 完整配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration><!-- 控制台输出 --><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--格式化输出: %d: 表示日期%thread: 表示线程名%-5level: level 是日志级别, -5 表示级别从左显示 5 个字符宽度以对齐%logger{50}: 传递的 Logger 对象, 50 是限制长度%msg: 日志消息%n: 换行符--><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern></encoder></appender><!-- 系统文件输出 --><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 日志文件输出的文件名, %i 表示序号 --><FileNamePattern>D:/tlias-web-management/src/main/resources/log/tlias-%d{yyyy-MM-dd}-%i.log</FileNamePattern><!-- 最多保留的历史日志文件数量 --><MaxHistory>30</MaxHistory><!-- 最大文件大小, 超过这个大小会触发滚动到新文件, 默认为 10 MB --><maxFileSize>10MB</maxFileSize></rollingPolicy><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--格式化输出: %d: 表示日期%thread: 表示线程名%-5level: level 是日志级别, -5 表示级别从左显示 5 个字符宽度以对齐%logger{50}: 传递的 Logger 对象, 50 是限制长度%msg: 日志消息%n: 换行符--><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern></encoder></appender><!-- 日志输出级别 --><root level="INFO"><appender-ref ref="STDOUT" /><appender-ref ref="FILE" /></root>
</configuration>
日志级别
指日志信息的类型
一般不选择 ALL, 而是 DEBUG 或者 INFO, 否则日志太多
多表
多表关系
一对多
场景: 一个部门 对 多个员工
最基础的思路: 在数据库表中"多"的一方 (子表), 添加字段如 dept_id
, 来关联"一"的一方 (父表) 的主键
但是问题是: 删除了部门, 被删除部门的员工仍然存在, 出现数据的不完整, 不一致了
原因在于: 这两张表, 在数据库层面并未建立关联
解决方案: 外键约束
- 创建表时添加外键约束
create table 表名(字段名 数据类型,...[constraint] [外键名称] foreign key (外键字段名) references 主表 (字段名)
);
- 建表后添加外键约束
alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表 (字段名);
但是开发不会用物理外键, 而是逻辑外键, 即在业务层逻辑中, 建立关联
一对一
场景: 一个用户 对 一个身份证信息
关系: 一对一关系, 多用于单表拆分, 比如用户信息表拆成 用户基本信息表 和 用户身份信息表 (如果在业务系统当中, 对用户的基本信息查询频率特别的高, 但是对于用户的身份信息查询频率很低, 此时出于提高查询效率的考虑, 就可以将这张大表拆分成两张小表, 第一张表存放的是用户的基本信息, 而第二张表存放的就是用户的身份信息)
实现: 在任意一方加上外键, 关联另一方的主键, 并且设置外键为唯一的
多对多
场景: 多个学生 对 多个课程
实现: 建立第三张表, 中间表至少包含两个外键, 分别关联两方主键
多表查询
select * from emp, dept where emp.dept_id = dept.id;
- 多表查询分类
- 连接查询
- 内连接: 查询 A, B 交集部分
- 外连接
- 左连接: 查询左表所有数据 (包含交集)
- 右连接: 查询右表所有数据 (包含交集)
- 子查询
- 嵌套
- 连接查询
连接查询
内连接
- 查询 性别为男, 且工资高于 8000 的员工的 ID, 姓名, 及所属的部门名称
select emp.id, emp.name, dept.name from emp where emp.dept_id = dept.id and emp.gender = 1 and emp.salary > 8000;
- 给表起别名简化书写
select 字段列表 from 表1 as 别名1 ,表2 as 别名2 where 条件 ... ;select 字段列表 from 表1 别名1, 表2 别名2 where 条件 ... ; -- as 可以省略
- 注意: 一旦为表起了别名, 就不能再使用表名来指定对应的字段了, 此时只能够使用别名来指定字段
select e.id, e.name, d.name from emp e, dept d where e.dept_id = d.id and e.gender = 1 and e.salary > 8000;
外连接
- 左连接 (查询表 1, 包含交集)
select 字段列表 from 表1 left [outer] join 表2 on 连接条件 ... ;
- 右连接 (查询表 2, 包含交集)
select 字段列表 from 表1 right [outer] join 表2 on 连接条件 ... ;
- 查询工资高于 8000 的 所有员工的姓名, 和对应的部门名称 (左外连接)
select e.name, d.name from emp e left join dept d on e.dept_id = d.id where e.salary > 8000;
- 与内连接的区别在于, 左连接时, 如果右表没有匹配的行, 则右表的列值用 NULL 填充, 而内连接只会返回匹配的行
子查询
- sql 语句嵌套
select * from t1 where column1 = (select column1 from t2 ...);
- 分类
- 标量子查询: 子查询返回的结果为单个值
- 列子查询: 子查询返回的结果为一列
- 行子查询: 子查询返回的结果为一行
- 表子查询: 子查询返回的结果为多行多列
- 注意是子查询返回的结果
标量子查询
- 查询最早入职员工
select * from emp where entry_date = (select min(entry_date) from emp);
- 查询在阮小五入职之后入职的员工
select * from emp where entry_date > (select entry_date from emp where name = '阮小五');
列子查询
- 查询教研部和咨询部的所有员工信息
select * from emp where dept_id in (select id from dept where name = '教研部' or name = '咨询部');
行子查询
- 查询与李忠的薪资以及职位都相同的员工信息
select * from emp where (salary, job) = (select salary, job from emp where name = '李忠');
表子查询
select * from emp e,(select dept_id, max(salary) max_sal from emp group by dept_id) awhere e.dept_id = a.dept_id and e.salary = a.max_sal;
其中子查询
select dept_id, max(salary) from emp group by dept_id;
是获取每个部门的最高薪资, 将这个作为临时表 a
事务管理
问题分析: 保存员工的基本信息成功, 但保存员工的工作经历信息失败, 就会导致数据库数据的不完整, 不一致
- 事务: 事务是一组操作的集合, 是一个不可分割的工作单位; 事务中的操作要么同时成功, 要么同时失败
三步操作
-
开启事务
start transaction; / begin;
-
提交事务
commit;
-
回滚事务
rollback;
-- 开启事务
start transaction; / begin;-- 1. 保存员工基本信息
insert into emp values (39, 'Tom', '123456', '汤姆', 1, '13300001111', 1, 4000, '1.jpg', '2023-11-01', 1, now(), now());-- 2. 保存员工的工作经历信息
insert into emp_expr(emp_id, begin, end, company, job) values (39,'2019-01-01', '2020-01-01', '百度', '开发'), (39,'2020-01-10', '2022-02-01', '阿里', '架构');-- 提交事务 (全部成功)
commit;-- 回滚事务 (有一个失败)
rollback;
Spring 事务管理
@Transactional
- 作用: 方法执行开始前开启事务; 执行完毕后提交事务; 如果方法执行过程中出现了异常, 就会进行事务的回滚
- 位置: 业务层 Service 的方法上, 类上, 接口上; 因为业务层中, 一个业务功能可能包含多个数据访问操作
@Transactional @Override public void save(Emp emp) {// 保存员工基本信息emp.setCreateTime(LocalDateTime.now());emp.setUpdateTime(LocalDateTime.now());empMapper.insert(emp);// 保存员工工作经历信息List<EmpExpr> exprList = emp.getExprList();if (!CollectionUtils.isEmpty(exprList)) {// 遍历集合, 为 empId 赋值exprList.forEach(empExpr -> empExpr.setEmpId(emp.getId()));// 批量保存员工工作经历信息empExprMapper.insertBatch(exprList);} }
- 在 application.yml 中开启事务管理日志
#spring事务管理日志
logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug
rollbackFor
默认情况下, 只有出现 RuntimeException 才会回滚事务
- 配置 rollbackFor 属性指定出现何种异常时回滚事务
@Transactional(rollbackFor = Exception.class)
propagation
事务的传播行为: 一个事务方法被另一个事务方法调用时, 这个事务方法应该如何进行事务控制
-
e.g. 新增员工信息时, 无论成功还是失败, 都要记录操作日志
-
创建数据库表
-- 创建员工日志表
create table emp_log(id int unsigned primary key auto_increment comment 'ID, 主键',operate_time datetime comment '操作时间',info varchar(2000) comment '日志信息'
) comment '员工日志表';
- EmpLog 实体类
package com.example.tlias_web_management.pojo;import java.time.LocalDateTime;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmpLog {private Integer id;private LocalDateTime operateTime;private String info;
}
- EmpLogMapper
package com.example.tlias_web_management.mapper;import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;import com.example.tlias_web_management.pojo.EmpLog;@Mapper
public interface EmpLogMapper {/** 插入日志*/@Insert("insert into emp_log (operate_time, info) values (#{operateTime}, #{info})")public void insert(EmpLog empLog);
}
- EmpLogService
package com.example.tlias_web_management.service;import com.example.tlias_web_management.pojo.EmpLog;public interface EmpLogService {/** 记录添加员工日志*/public void insertLog(EmpLog empLog);
}
- EmpLogServiceImpl
package com.example.tlias_web_management.service.impl;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import com.example.tlias_web_management.mapper.EmpLogMapper;
import com.example.tlias_web_management.pojo.EmpLog;
import com.example.tlias_web_management.service.EmpLogService;@Service
public class EmpLogServiceImpl implements EmpLogService {@Autowiredprivate EmpLogMapper empLogMapper;@Transactional@Overridepublic void insertLog(EmpLog empLog) {empLogMapper.insert(empLog);}
}
- EmpServiceImpl
package com.example.tlias_web_management.service.impl;import java.time.LocalDateTime;
import java.util.List;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;import com.example.tlias_web_management.mapper.EmpExprMapper;
import com.example.tlias_web_management.mapper.EmpMapper;
import com.example.tlias_web_management.pojo.Emp;
import com.example.tlias_web_management.pojo.EmpExpr;
import com.example.tlias_web_management.pojo.EmpLog;
import com.example.tlias_web_management.pojo.EmpQueryParam;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.service.EmpLogService;
import com.example.tlias_web_management.service.EmpService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;@Service
public class EmpServiceImpl implements EmpService {@Autowiredprivate EmpMapper empMapper;@Autowiredprivate EmpExprMapper empExprMapper;@Autowiredprivate EmpLogService empLogService;@Overridepublic PageResult<Emp> page(EmpQueryParam empQueryParam) {// 设置分页参数 (PageHelper)PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize());// 执行查询List<Emp> empList = empMapper.list(empQueryParam);// 解析查询结果, 并封装Page<Emp> p = (Page<Emp>) empList;return new PageResult<Emp>(p.getTotal(), p.getResult());}@Transactional@Overridepublic void save(Emp emp) {try {// 保存员工基本信息emp.setCreateTime(LocalDateTime.now());emp.setUpdateTime(LocalDateTime.now());empMapper.insert(emp);// 保存员工工作经历信息List<EmpExpr> exprList = emp.getExprList();if (!CollectionUtils.isEmpty(exprList)) {// 遍历集合, 为 empId 赋值exprList.forEach(empExpr -> empExpr.setEmpId(emp.getId()));// 批量保存员工工作经历信息empExprMapper.insertBatch(exprList);}} finally {// 记录操作日志EmpLog empLog = new EmpLog(null, LocalDateTime.now(), emp.toString());empLogService.insertLog(empLog);}}
}
-
如果没有设置 propagation, 默认为 REQUIRED, 表示有事务则加入, 没有才新建事务, 因此出现异常时就会回滚 save 和 insertLog 操作, 导致出现异常时没有记录操作日志
-
将 propagation 设置为 REQUIRES_NEW: 无论是否有事务, 都创建新事务, 运行在一个独立的事务中
-
修改于业务层 EmpLogServiceImpl
package com.example.tlias_web_management.service.impl;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;import com.example.tlias_web_management.mapper.EmpLogMapper;
import com.example.tlias_web_management.pojo.EmpLog;
import com.example.tlias_web_management.service.EmpLogService;@Service
public class EmpLogServiceImpl implements EmpLogService {@Autowiredprivate EmpLogMapper empLogMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)@Overridepublic void insertLog(EmpLog empLog) {empLogMapper.insert(empLog);}
}
事务四大特性
文件上传
接口开发 (保存到本地)
- 前端代码, 将其复制到 static 目录下
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>上传文件</title>
</head>
<body><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="file"><br><input type="submit" value="提交"></form></body>
</html>
- UploadController, 注意形参名和请求参数名称一致; MultipartFile 为 Spring 提供的 api, 可以接收到上传的文件
package com.example.tlias_web_management.controller;import java.io.File;import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import com.example.tlias_web_management.pojo.Result;import lombok.extern.slf4j.Slf4j;@Slf4j
@RestController
public class UploadController {/** 上传文件 - 参数名 file* POST* http://localhost:8080/upload*/@PostMapping("/upload")public Result upload(String username, Integer age, MultipartFile file) throws Exception {log.info("上传文件: {}, {}, {}", username, age, file);if (!file.isEmpty()) {file.transferTo(new File("D:\\tlias_images\\" + file.getOriginalFilename()));}return Result.success();}
}
- 默认单个文件最大大小为 1M, 可以在 application.yml 中配置
spring:servlet:multipart:max-file-size: 10MBmax-request-size: 100MB
- 如果目标目录不存在, 创建目录; 保证上传的文件名不重复
package com.example.tlias_web_management.controller;import java.io.File;
import java.util.UUID;import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import com.example.tlias_web_management.pojo.Result;import lombok.extern.slf4j.Slf4j;@Slf4j
@RestController
public class UploadController {private static final String UPLOAD_DIR = "D:/tlias_images/";/** 上传文件 - 参数名 file* POST* http://localhost:8080/upload*/@PostMapping("/upload")public Result upload(String username, Integer age, MultipartFile file) throws Exception {log.info("上传文件: {}, {}, {}", username, age, file);if (!file.isEmpty()) {// 生成唯一文件名String originalFilename = file.getOriginalFilename();String extName = originalFilename.substring(originalFilename.lastIndexOf("."));String uniqueFilename = UUID.randomUUID().toString().replace("-", "") + extName;// 拼接完整文件路径File targetFile = new File(UPLOAD_DIR + uniqueFilename);// 如果目标目录不存在, 创建目录if (!targetFile.getParentFile().exists()) {targetFile.getParentFile().mkdirs();}// 保存文件file.transferTo(targetFile);}return Result.success();}
}
阿里云 OSS
准备工作
- 注册账号
- 开启对象存储 OSS 服务
- 创建 Bucket
- 创建 OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET
- 配置 OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET 到环境变量
入门程序
- 配置依赖
<!--阿里云 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>
- 上传文件
package com.example.tlias_web_management;import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.file.Files;public class OSSTest {public static void main(String[] args) throws Exception {// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。String endpoint = "https://oss-cn-shenzhen.aliyuncs.com";// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();// 填写Bucket名称,例如examplebucket。String bucketName = "xxxx";// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。String objectName = "001.jpg";// 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。String region = "cn-shenzhen";// 创建OSSClient实例。ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);OSS ossClient = OSSClientBuilder.create().endpoint(endpoint).credentialsProvider(credentialsProvider).clientConfiguration(clientBuilderConfiguration).region(region).build();try {File file = new File("D:\\tlias_images\\IMG_2287.JPG");byte[] content = Files.readAllBytes(file.toPath());ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));} 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();}}}
}
接口开发 (保存到阿里云 OSS)
需求
- 响应数据 e.g.
{"code": 1,"msg": "success","data": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-0400.jpg"
}
实现
- 引入阿里云 OSS 上传文件工具类于 utils 包下
package com.example.tlias_web_management.utils;import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;@Component
public class AliyunOSSOperator {private String endpoint = "https://oss-cn-shenzhen.aliyuncs.com";private String bucketName = "xxxx";private String region = "cn-shenzhen";public String upload(byte[] content, String originalFilename) throws Exception {// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();// 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。//获取当前系统日期的字符串,格式为 yyyy/MMString dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));//生成一个新的不重复的文件名String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));String objectName = dir + "/" + newFileName;// 创建OSSClient实例。ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);OSS ossClient = OSSClientBuilder.create().endpoint(endpoint).credentialsProvider(credentialsProvider).clientConfiguration(clientBuilderConfiguration).region(region).build();try {ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));} finally {ossClient.shutdown();}return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;}
}
- UploadController
package com.example.tlias_web_management.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import com.example.tlias_web_management.pojo.Result;
import com.example.tlias_web_management.utils.AliyunOSSOperator;import lombok.extern.slf4j.Slf4j;@Slf4j
@RestController
public class UploadController {@Autowiredprivate AliyunOSSOperator aliyunOSSOperator;/** 上传文件 - 参数名 file* POST* http://localhost:8080/upload*/@PostMapping("/upload")public Result upload(MultipartFile file) throws Exception {log.info("文件上传: {}", file.getOriginalFilename());String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());log.info("文件上传 OSS, url: {}", url);return Result.success(url);}
}
Apifox 测试
优化
-
配置到 application.yml 中; 通过
@Value
注入属性值- 将 endpoint, bucketName, region 配置到 application.yml 中
# 阿里云 OSS aliyun: oss:endpoint: https://oss-cn-shenzhen.aliyuncs.combucketName: xxxxregion: cn-shenzhen
- 修改 AliyunOSSOperator:
@Value
注解来注入外部配置的属性
@Value("${aliyun.oss.endpoint}") private String endpoint; @Value("${aliyun.oss.bucketName}") private String bucketName; @Value("${aliyun.oss.region}") private String region;
-
配置到 application.yml 中; 创建一个实体类并添加
@ConfigurationProperties
注解获取对应的属性值; 将实体类交给 IOC 管理, 通过 bean 对象的 get 方法获取属性值- 将 endpoint, bucketName, region 配置到 application.yml 中
# 阿里云 OSS aliyun: oss:endpoint: https://oss-cn-shenzhen.aliyuncs.combucketName: xxxxregion: cn-shenzhen
- 创建实体类: 指定配置参数项的前缀, 属性名和配置参数项的名称一致
package com.example.tlias_web_management.utils;import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;import lombok.Data;@Data @Component @ConfigurationProperties(prefix = "aliyun.oss") public class AliyunOSSProperties {private String endpoint;private String bucketName;private String region; }
- 修改 AliyunOSSOperator
package com.example.tlias_web_management.utils;import com.aliyun.oss.*; import com.aliyun.oss.common.auth.CredentialsProviderFactory; import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider; import com.aliyun.oss.common.comm.SignVersion;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.UUID;@Component public class AliyunOSSOperator {@Autowiredprivate AliyunOSSProperties aliyunOSSProperties;public String upload(byte[] content, String originalFilename) throws Exception {String endpoint = aliyunOSSProperties.getEndpoint();String bucketName = aliyunOSSProperties.getBucketName();String region = aliyunOSSProperties.getRegion();// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();// 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。//获取当前系统日期的字符串,格式为 yyyy/MMString dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));//生成一个新的不重复的文件名String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));String objectName = dir + "/" + newFileName;// 创建OSSClient实例。ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);OSS ossClient = OSSClientBuilder.create().endpoint(endpoint).credentialsProvider(credentialsProvider).clientConfiguration(clientBuilderConfiguration).region(region).build();try {ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));} finally {ossClient.shutdown();}return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;} }
异常处理
package com.example.tlias_web_management.exception;import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;import com.example.tlias_web_management.pojo.Result;import lombok.extern.slf4j.Slf4j;@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/** 处理异常*/@ExceptionHandlerpublic Result handleException(Exception e) { // 形参指定能够处理的异常类型log.error("程序出错", e);return Result.error("出错, 请联系管理员");}
}
- 更为精准的处理违反唯一键约束的异常
- 异常会从下至上匹配处理异常的方法
/** 处理违反唯一键约束的异常*/ @ExceptionHandler public Result handleDuplicateKeyException(DuplicateKeyException e) {log.error("程序出错", e);String message = e.getMessage();int i = message.indexOf("Duplicate entry");String errMsg = message.substring(i);String[] arr = errMsg.split(" ");return Result.error(arr[2] + "已存在"); }
员工管理
准备工作
-
创建表
-
准备实体类 Emp, EmpExpr
-
EmpController
-
EmpService, EmpServiceImpl
-
EmpMapper
列表查询
需求
-
前四个参数不必须, 后两个参数必须, 只有后两个参数时即为非条件分页查询
-
注意后台给前端返回的数据要包含什么内容
接口开发 - 非条件查询
- PageResult 封装后台给前端返回的数据: 数据列表和总记录数
- 注意: 属性名要和接口文档中名称一致
- 使用了泛型
package com.example.tlias_web_management.pojo;import java.util.List;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/** 分页结果封装类*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult<T> {private long total; // 总记录数private List<T> rows; // 当前页数据列表
}
- Controller
- 不指定时, 默认 page = 1, pageSize = 10
package com.example.tlias_web_management.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import com.example.tlias_web_management.pojo.Emp;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.pojo.Result;
import com.example.tlias_web_management.service.EmpService;import lombok.extern.slf4j.Slf4j;/** 员工管理控制器*/
@Slf4j
@RequestMapping("/emps")
@RestController
public class EmpController {@Autowiredprivate EmpService empService;/** 员工分页查询* GET* http://localhost:8080/emps?page=1&pageSize=10*/@GetMappingpublic Result page(@RequestParam(defaultValue = "1") Integer page,@RequestParam(defaultValue = "10") Integer pageSize) {log.info("分页查询: {}, {}", page, pageSize);PageResult<Emp> pageResult = empService.page(page, pageSize);return Result.success(pageResult);}
}
- Service
- 分页查询使用 PageHelper 插件, 简化分页操作, 提高开发效率 (无需在 Mapper 进行手动分页查询, 只需在 Service 层设置分页参数)
- 注意: PageHelper 只会对紧跟在其后的第一个查询语句进行分页处理
- 如果传递不合法页码参数, 比如查询 -1 页, 或者查询的页码超过总数, 就会查询失败, 所以要在 application.yml 中添加 PageHelper 合理化参数配置: reasonable 为 true 时, <=0 则查询第一页, 超过则查询最后一页
pagehelper:reasonable: truehelper-dialect: mysql
package com.example.tlias_web_management.service.impl;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import com.example.tlias_web_management.mapper.EmpMapper;
import com.example.tlias_web_management.pojo.Emp;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.service.EmpService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;@Service
public class EmpServiceImpl implements EmpService {@Autowiredprivate EmpMapper empMapper;@Overridepublic PageResult<Emp> page(Integer page, Integer pageSize) {// 设置分页参数 (PageHelper)PageHelper.startPage(page, pageSize);// 执行查询List<Emp> empList = empMapper.list();// 解析查询结果, 并封装Page<Emp> p = (Page<Emp>) empList;return new PageResult<Emp>(p.getTotal(), p.getResult());}
}
- Mapper
- 注意 d.name 对应 deptName 但不符合转换规则 Mybatis 无法自动转换, 因此要起别名, 别名与属性名 deptName 一致
- 注意使用 PageHelper 时, 定义的 SQL 语句结尾不能加分号, 因为还要拼接 limit
- 注意为了美观将一句 SQL 分成多句时, 某些句子末尾的空格不能漏, 否则拼接的语句错误
package com.example.tlias_web_management.mapper;import java.util.List;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;import com.example.tlias_web_management.pojo.Emp;@Mapper
public interface EmpMapper {/** 分页查询*/@Select("select e.*, d.name deptName from emp e left join dept d " + "on e.dept_id = d.id order by e.update_time desc")public List<Emp> list();
}
接口开发 - 条件查询
- 条件查询有六个参数, 定义实体类封装
- 需要保证前端传递的请求参数和实体类的属性名一致
package com.example.tlias_web_management.pojo;import java.time.LocalDate;import org.springframework.format.annotation.DateTimeFormat;import lombok.Data;/** 条件分页查询参数封装类*/
@Data
public class EmpQueryParam {private Integer page = 1; // 默认private Integer pageSize = 10; // 默认private String name;private Integer gender;@DateTimeFormat(pattern = "yyyy-MM-dd")private LocalDate begin;@DateTimeFormat(pattern = "yyyy-MM-dd")private LocalDate end;
}
- Controller
package com.example.tlias_web_management.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import com.example.tlias_web_management.pojo.Emp;
import com.example.tlias_web_management.pojo.EmpQueryParam;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.pojo.Result;
import com.example.tlias_web_management.service.EmpService;import lombok.extern.slf4j.Slf4j;/** 员工管理控制器*/
@Slf4j
@RequestMapping("/emps")
@RestController
public class EmpController {@Autowiredprivate EmpService empService;/** 条件分页查询* GET* http://localhost:8080/emps?name=张&gender=1&begin=2007-09-01&end=2022-09-01&page=1&pageSize=10*/@GetMappingpublic Result page(EmpQueryParam empQueryParam) {log.info("查询请求参数: {}", empQueryParam);PageResult<Emp> pageResult = empService.page(empQueryParam);return Result.success(pageResult);}
}
- Service
package com.example.tlias_web_management.service.impl;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import com.example.tlias_web_management.mapper.EmpMapper;
import com.example.tlias_web_management.pojo.Emp;
import com.example.tlias_web_management.pojo.EmpQueryParam;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.service.EmpService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;@Service
public class EmpServiceImpl implements EmpService {@Autowiredprivate EmpMapper empMapper;@Overridepublic PageResult<Emp> page(EmpQueryParam empQueryParam) {// 设置分页参数 (PageHelper)PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize());// 执行查询List<Emp> empList = empMapper.list(empQueryParam);// 解析查询结果, 并封装Page<Emp> p = (Page<Emp>) empList;return new PageResult<Emp>(p.getTotal(), p.getResult());}
}
- Mapper
- 条件查询 sql 语句比较复杂, 采用 xml 映射
- 需求: 姓名模糊匹配 e.g.
e.name like '%阮%'
- 但
#{...}
不能用在引号中, 因此不能写为e.name like '%#{name}%'
- 应当使用 sql 提供的拼接函数 concat
- 需求: 姓名模糊匹配 e.g.
- 条件查询 sql 语句比较复杂, 采用 xml 映射
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.tlias_web_management.mapper.EmpMapper"><!-- resultType: 查询返回的单条记录的类型 --><select id="list" resultType="com.example.tlias_web_management.pojo.Emp"> select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.idwhere e.name like concat('%', #{name}, '%') and e.gender = #{gender}and e.entry_date between #{begin} and #{end}order by e.update_time desc</select>
</mapper>
- 上面的代码, 在有的参数没有传递值时, 仍然对这个参数进行了判断, 导致查询失败, 不符合需求
- 需要使用动态 sql 语句
<if>
: 判断条件是否成立, 如果条件为 true, 拼接 SQL
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.tlias_web_management.mapper.EmpMapper"><!-- resultType: 查询返回的单条记录的类型 --><select id="list" resultType="com.example.tlias_web_management.pojo.Emp"> select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.idwhere<if test="name != null and name != ''">e.name like concat('%', #{name}, '%')</if><if test="gender != null">and e.gender = #{gender}</if><if test="begin != null and end != null">and e.entry_date between #{begin} and #{end}</if>order by e.update_time desc</select> </mapper>
- 上面的代码还是有问题: 如果没有拼接
e.name like concat('%', #{name}, '%')
, 那么就可能出现where and e.gender = #{gender}
这样的错误语句; 如果三个句子都没拼接, 还可能出现where order by e.update_time desc
这样的错误语句 - 因此要使用
<where>
: 当它包含的条件都不成立时, 会自动去除 WHERE 关键字; 当有条件成立时, 会自动去除条件前面多余的 AND 或 OR 关键字
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.tlias_web_management.mapper.EmpMapper"><!-- resultType: 查询返回的单条记录的类型 --><select id="list" resultType="com.example.tlias_web_management.pojo.Emp"> select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id<where><if test="name != null and name != ''">e.name like concat('%', #{name}, '%')</if><if test="gender != null">and e.gender = #{gender}</if><if test="begin != null and end != null">and e.entry_date between #{begin} and #{end}</if></where>order by e.update_time desc</select> </mapper>
- 需要使用动态 sql 语句
Apifox 测试
- 注意 EmpMapper.xml 的位置要在 resources/com/example/tlias_web_management/mapper 下
- 注意 sql 语句不要写错
添加员工
需求
需要将员工基本信息保存到 emp, 员工工作经历信息保存到 emp_expr
- 请求数据样例
{"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,"phone": "18809091234","salary": 8000,"exprList": [{"company": "百度科技股份有限公司","job": "java开发","begin": "2012-07-01","end": "2019-03-03"},{"company": "阿里巴巴科技股份有限公司","job": "架构师","begin": "2019-03-15","end": "2023-03-01"}]
}
接口开发
- 修改员工信息实体类 Emp
package com.example.tlias_web_management.pojo;import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;import lombok.Data;@Data
public class Emp {private Integer id;private String username;private String password;private String name;private Integer gender;private String phone;private Integer job;private Integer salary;private String image;private LocalDate entryDate;private Integer deptId;private LocalDateTime createTime;private LocalDateTime updateTime;// 封装部门名称private String deptName;// 封装工作经历信息private List<EmpExpr> exprList;}
-
Controller
/** 添加员工* POST* http://localhost:8080/emps 请求体*/ @PostMapping public Result add(@RequestBody Emp emp) {log.info("添加员工: {}", emp);empService.save(emp);return Result.success(); }
-
Service
- 需要保存到两个表
@Override public void save(Emp emp) {// 保存员工基本信息emp.setCreateTime(LocalDateTime.now());emp.setUpdateTime(LocalDateTime.now());empMapper.insert(emp);// 保存员工工作经历信息List<EmpExpr> exprList = emp.getExprList();if (!CollectionUtils.isEmpty(exprList)) {// 遍历集合, 为 empId 赋值exprList.forEach(empExpr -> empExpr.setEmpId(emp.getId()));// 批量保存员工工作经历信息empExprMapper.insertBatch(exprList);} }
-
Mapper
- EmpMapper, 需要返回当前添加数据后生成的主键 id
/** 添加员工基本信息*/ @Options(useGeneratedKeys = true, keyProperty = "id") // 获取到生成的主键 -- 主键返回, 赋值给 emp.id @Insert("insert into emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) " +"values(#{username}, #{name}, #{gender}, #{phone}, #{job}, #{salary}, #{image}, #{entryDate}, #{deptId}, #{createTime}, #{updateTime})") public void insert(Emp emp);
- EmpExprMapper, 采用 xml, 使用动态 sql 语句
<foreach>
(collection 集合名称, item 集合遍历出来的元素/项, separator 每一次遍历使用的分隔符[可选], open 遍历开始前拼接的片段[可选], close 遍历结束后拼接的片段[可选])
/** 批量添加员工工作经历信息*/ void insertBatch(List<EmpExpr> exprList);
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.tlias_web_management.mapper.EmpExprMapper"><insert id="insertBatch">insert into emp_expr(emp_id, begin, end, company, job) values<foreach collection="exprList" item="expr" separator=",">(#{expr.empId}, #{expr.begin}, #{expr.end}, #{expr.company}, #{expr.job})</foreach></insert> </mapper>
Apifox 测试
删除员工
需求
- 批量删除员工的数据信息, 批量删除包含删除一个
接口开发
-
Controller
- Java Web (2024) 接口文档采用查询参数, 但 2023 提供的前端环境采用的是路径参数
/** 批量删除员工* DELETE* http://localhost:8080/emps?ids=1,2,3*/ @DeleteMapping public Result delete(@RequestParam List<Integer> ids) {log.info("批量删除员工: ids = {}", ids);empService.deleteByIds(ids);return Result.success(); }
-
Service
- 删除员工涉及多个表的删除, 需要事务管理
@Transactional @Override public void deleteByIds(List<Integer> ids) {// 删除员工基本信息empMapper.deleteByIds(ids);// 删除员工工作经历信息empExprMapper.deleteByEmpIds(ids); }
-
Mapper
- EmpMapper.xml
<!-- deleteByIds --> <delete id="deleteByIds">delete from emp where id in<foreach collection="ids" item="id" open="(" close=")" separator=",">#{id}</foreach> </delete>
- EmpExprMapper.xml
<!-- deleteByEmpIds --> <delete id="deleteByEmpIds">delete from emp_expr where emp_id in<foreach collection="empIds" item="empId" open="(" close=")" separator=",">#{empId}</foreach> </delete>
Apifox 测试
修改员工
查询回显
需求
- 路径参数
接口开发
-
Controller
- 采用路径参数
/** 根据 ID 查询员工* GET* http://localhost:8080/emps/1*/ @GetMapping("/{id}") public Result getInfo(@PathVariable Integer id) {log.info("根据 ID 查询员工的详细信息: {}", id);Emp emp = empService.getInfo(id);return Result.success(emp); }
-
Service
- 分别调用 EmpMapper 和 EmpExprMapper
@Override public Emp getInfo(Integer id) {Emp emp = empMapper.getById(id);List<EmpExpr> exprList = empExprMapper.getByEmpId(id);emp.setExprList(exprList);return emp; }
-
Mapper
- EmpMapper
/** 根据 ID 查询员工基本信息*/ public Emp getById(Integer id);
- EmpExprMapper
/** 根据 ID 查询员工工作经历信息*/ @Select("select * from emp_expr where emp_id = #{empId}") List<EmpExpr> getByEmpId(Integer empId);
-
感觉繁琐而没采用课程提供的方案: Service 层只调用 EmpMapper 进行多表查询, 然后手动封装结果
<!--自定义结果集ResultMap-->
<resultMap id="empResultMap" type="com.itheima.pojo.Emp"><id column="id" property="id" /><result column="username" property="username" /><result column="password" property="password" /><result column="name" property="name" /><result column="gender" property="gender" /><result column="phone" property="phone" /><result column="job" property="job" /><result column="salary" property="salary" /><result column="image" property="image" /><result column="entry_date" property="entryDate" /><result column="dept_id" property="deptId" /><result column="create_time" property="createTime" /><result column="update_time" property="updateTime" /><!--封装exprList--><collection property="exprList" ofType="com.itheima.pojo.EmpExpr"><id column="ee_id" property="id"/><result column="ee_company" property="company"/><result column="ee_job" property="job"/><result column="ee_begin" property="begin"/><result column="ee_end" property="end"/><result column="ee_empid" property="empId"/></collection>
</resultMap><!--根据ID查询员工的详细信息-->
<select id="getById" resultMap="empResultMap">select e.*,ee.id ee_id,ee.emp_id ee_empid,ee.begin ee_begin,ee.end ee_end,ee.company ee_company,ee.job ee_jobfrom emp e left join emp_expr ee on e.id = ee.emp_idwhere e.id = #{id}
</select>
Apifox 测试
前后端联调测试
修改员工
需求
- 采用 PUT, 请求体
接口开发
-
Controller
- 采用 PUT, 请求体
/** 根据 ID 修改员工* PUT* http://localhost:8080/emps 请求体*/ @PutMapping public Result update(@RequestBody Emp emp) {log.info("修改员工: {}", emp);empService.update(emp);return Result.success(); }
-
Service
- 基本信息只有一条, 直接更新
- 工作经历信息可能有多条, 采取先删除, 后添加
- 需要事务管理
@Transactional(rollbackFor = Exception.class) @Override public void update(Emp emp) {// 根据 ID 修改员工基本信息emp.setUpdateTime(LocalDateTime.now());empMapper.updateById(emp);// 根据 ID 修改员工工作经历信息 (可能有多条, 采用先删除, 后添加)// 删除旧的工作经历empExprMapper.deleteByEmpIds(Arrays.asList(emp.getId())); // 调用已有的方法// 添加新的工作经历List<EmpExpr> exprList = emp.getExprList();if (!CollectionUtils.isEmpty(exprList)) {exprList.forEach(empExpr -> empExpr.setEmpId(emp.getId())); // 确保设置了员工的 idempExprMapper.insertBatch(exprList); // 调用已有的方法} }
-
Mapper
- EmpMapper
<update id="updateById">update Empsetusername = #{username},password = #{password},name = #{name},gender = #{gender},phone = #{phone},job = #{job},salary = #{salary},image = #{image},entry_date = #{entryDate},dept_id = #{deptId},update_time = #{updateTime}where id = #{id} </update>
-
EmpExprMapper 已有批量删除和批量添加的方法
-
如果没有被修改的信息在传递给后端的请求数据中为 null, 或者要求"被修改为 null 的数据不更新为 null, 而是保留原值", 则要使用动态 sql
<update id="updateById">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="phone != null and phone != ''">phone = #{phone},</if><if test="job != null">job = #{job},</if><if test="salary != null">salary = #{salary},</if><if test="image != null and image != ''">image = #{image},</if><if test="entryDate != null">entry_date = #{entryDate},</if><if test="deptId != null">dept_id = #{deptId},</if><if test="updateTime != null">update_time = #{updateTime},</if></set>where id = #{id} </update>
Apifox 测试
前后端联调测试
员工信息统计
职位统计
需求
接口开发
- 准备实体类 JobOption
package com.example.tlias_web_management.pojo;import java.util.List;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class JobOption {private List<Object> jobList;private List<Object> dataList;
}
- Controller
package com.example.tlias_web_management.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import com.example.tlias_web_management.pojo.JobOption;
import com.example.tlias_web_management.pojo.Result;
import com.example.tlias_web_management.service.ReportService;import lombok.extern.slf4j.Slf4j;@Slf4j
@RequestMapping("/report")
@RestController
public class ReportController {@Autowiredprivate ReportService reportService;/** 员工职位统计* GET* http://localhost:8080/report/empJobData*/@GetMapping("/empJobData")public Result getEmpJobData() {log.info("统计各个职位的员工人数");JobOption jobOption = reportService.getEmpJobData();return Result.success(jobOption);}
}
- Service
package com.example.tlias_web_management.service.impl;import java.util.List;
import java.util.Map;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import com.example.tlias_web_management.mapper.EmpMapper;
import com.example.tlias_web_management.pojo.JobOption;
import com.example.tlias_web_management.service.ReportService;@Service
public class ReportServiceImpl implements ReportService {@Autowiredprivate EmpMapper empMapper;@Overridepublic JobOption getEmpJobData() {List<Map<String, Object> > list = empMapper.countEmpJobData();List<Object> jobList = list.stream().map(dataMap -> dataMap.get("job")).toList();List<Object> dataList = list.stream().map(dataMap -> dataMap.get("total")).toList();return new JobOption(jobList, dataList);}
}
- Mapper
@MapKey("job")
可以指定 Map 的键
/** 统计各职位的员工人数*/ @MapKey("job") public List<Map<String, Object> > countEmpJobData();
- 使用 case
<select id="countEmpJobData" resultType="java.util.Map">select(case job when 1 then '班主任' when 2 then '讲师' when 3 then '学工主管' when 4 then '教研主管' when 5 then '咨询师' else '其他'end) job,count(*) totalfrom emp group by job order by total </select>
Apifox 测试
性别统计
需求
接口开发
-
Controller
- 注意接口文档要求返回的是键值对, 因此 List 中存放 Map
/** 员工性别统计* GET* http://localhost:8080/report/empGenderData*/ @GetMapping("/empGenderData") public Result getEmpGenderData() {log.info("统计员工性别信息");List<Map<String, Object> > genderList = reportService.getEmpGenderData();return Result.success(genderList); }
-
Service
@Override public List<Map<String, Object> > getEmpGenderData() {return empMapper.countEmpGenderData(); }
-
Mapper
- java
/** 统计员工性别信息*/ @MapKey("name") public List<Map<String, Object> > countEmpGenderData();
- xml: 使用 if 语句
<select id="countEmpGenderData" resultType="java.util.Map">select if (gender = 1, '男', '女') name,count(*) valuefrom emp group by gender </select>
- 注意接口文档要求返回的键值对中, 键的名称为 name, 不能随意地改成 gender 之类的
Apifox 测试
班级管理与学生管理的准备工作
-
创建数据库表 clazz, student
-
创建实体类 Clazz, Student
班级管理
列表查询
需求
-
需要定义实体类 ClazzQueryParam 来封装查询参数
-
需要用到之前已经实现的实体类 PageResult
接口开发
- ClazzQueryParam
package com.example.tlias_web_management.pojo;import java.time.LocalDate;import org.springframework.format.annotation.DateTimeFormat;import lombok.Data;/** 班级条件分页查询参数封装类*/
@Data
public class ClazzQueryParam {private String name;@DateTimeFormat(pattern = "yyyy-MM-dd")private LocalDate begin;@DateTimeFormat(pattern = "yyyy-MM-dd")private LocalDate end;private Integer page = 1;private Integer pageSize = 10;
}
- Controller
package com.example.tlias_web_management.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import com.example.tlias_web_management.pojo.Clazz;
import com.example.tlias_web_management.pojo.ClazzQueryParam;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.pojo.Result;
import com.example.tlias_web_management.service.ClazzService;import lombok.extern.slf4j.Slf4j;@Slf4j
@RequestMapping("/clazzs")
@RestController
public class ClazzController {@Autowiredprivate ClazzService clazzService;/** 条件分页查询* GET* http://localhost:8080/clazzs?name=java&begin=2023-01-01&end=2023-06-30&page=1&pageSize=5*/@GetMappingpublic Result page(ClazzQueryParam clazzQueryParam) {log.info("班级查询请求参数: {}", clazzQueryParam);PageResult<Clazz> pageResult = clazzService.page(clazzQueryParam);return Result.success(pageResult);}
}
- Service: 注意要补全 status
package com.example.tlias_web_management.service.impl;import java.time.LocalDate;
import java.util.List;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;import com.example.tlias_web_management.mapper.ClazzMapper;
import com.example.tlias_web_management.pojo.Clazz;
import com.example.tlias_web_management.pojo.ClazzQueryParam;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.service.ClazzService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;@Service
public class ClazzServiceImpl implements ClazzService {@Autowiredprivate ClazzMapper clazzMapper;@Overridepublic PageResult<Clazz> page(ClazzQueryParam clazzQueryParam) {// 设置分页参数 (PageHelper)PageHelper.startPage(clazzQueryParam.getPage(), clazzQueryParam.getPageSize());// 执行查询List<Clazz> clazzList = clazzMapper.list(clazzQueryParam);// 根据当前时间, 补全 statusif (!CollectionUtils.isEmpty(clazzList)) {clazzList.forEach(clazz -> {if (LocalDate.now().isAfter(clazz.getEndDate())) {clazz.setStatus("已结课");} else if (LocalDate.now().isBefore(clazz.getBeginDate())) {clazz.setStatus("未开班");} else {clazz.setStatus("在读中");}});}// 解析查询结果, 并封装Page<Clazz> p = (Page<Clazz>) clazzList;return new PageResult<Clazz>(p.getTotal(), p.getResult());}
}
- Mapper
- java
package com.example.tlias_web_management.mapper;import java.util.List;import org.apache.ibatis.annotations.Mapper;import com.example.tlias_web_management.pojo.Clazz; import com.example.tlias_web_management.pojo.ClazzQueryParam;@Mapper public interface ClazzMapper {/** 分页查询*/List<Clazz> list(ClazzQueryParam clazzQueryParam);}
- xml: 由接口文档可知, begin 和 end 是匹配结课时间的时间范围
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.tlias_web_management.mapper.ClazzMapper"><!-- resultType: 查询返回的单条记录的类型 --><!-- list --><select id="list" resultType="com.example.tlias_web_management.pojo.Clazz">select c.*, e.name masterName from clazz c left join emp e on c.master_id = e.id<where><if test="name != null and name != ''">c.name like concat('%', #{name}, '%')</if><if test="begin != null and end != null">and c.end_date between #{begin} and #{end}</if></where>order by c.update_time desc</select></mapper>
Apifox 测试
查询所有员工
略
新增班级
需求
- 采用请求体
接口开发
-
Controller
- 使用请求体; 别漏了
@PostMapping
/** 新增班级* POST* http://localhost:8080/clazzs 请求体*/ @PostMapping public Result add(@RequestBody Clazz clazz) {log.info("新增班级: {}", clazz);clazzService.save(clazz);return Result.success(); }
- 使用请求体; 别漏了
-
Service
- 记得补全 createTime, updateTime
@Override public void save(Clazz clazz) {// 补全基础属性 createTime, updateTimeclazz.setCreateTime(LocalDateTime.now());clazz.setUpdateTime(LocalDateTime.now());clazzMapper.insert(clazz); }
-
Mapper
/** 新增班级*/ @Insert("insert into clazz (name, room, begin_date, end_date, master_id, subject, create_time, update_time) " + "values (#{name}, #{room}, #{beginDate}, #{endDate}, #{masterId}, #{subject}, #{createTime}, #{updateTime})") void insert(Clazz clazz);
Apifox 测试
-
已存在 166 期, 测试唯一约束
-
改为 168 期, 则新增成功
根据 ID 查询班级
需求
- 采用路径参数
接口开发
-
Controller
- 采用路径参数
/** 根据 ID 查询班级* GET* http://localhost:8080/clazzs/8*/ @GetMapping("/{id}") public Result getInfo(@PathVariable Integer id) {log.info("根据 ID 查询班级: {}", id);Clazz clazz = clazzService.getById(id);return Result.success(clazz); }
-
Service
@Override public Clazz getById(Integer id) {return clazzMapper.getById(id); }
-
Mapper
/** 根据 ID 查询班级*/ @Select("select * from clazz c where c.id = #{id}") Clazz getById(Integer id);
Apifox 测试
修改班级
需求
接口开发
-
Controller
- 采用请求体
/** 修改班级* PUT* http://localhost:8080/clazzs*/ @PutMapping public Result update(@RequestBody Clazz clazz) {log.info("修改班级: {}", clazz);clazzService.update(clazz);return Result.success(); }
-
Service
- 注意补全修改时间
@Override public void update(Clazz clazz) {// 补全修改时间clazz.setUpdateTime(LocalDateTime.now());clazzMapper.updateById(clazz); }
-
Mapper
- xml: 注意
<if></if>
中字段更新语句需要逗号分隔
<update id="updateById">update clazz<set><if test="name != null and name != ''">name = #{name},</if><if test="room != null and room != ''">room = #{room},</if><if test="beginDate != null">begin_date = #{beginDate},</if><if test="endDate != null">end_date = #{endDate},</if><if test="masterId != null">master_id = #{masterId},</if><if test="subject != null">subject = #{subject},</if><if test="updateTime != null">update_time = #{updateTime},</if></set>where id = #{id} </update>
- xml: 注意
Apifox 测试
删除班级
需求
- 注意
接口开发
- 自定义异常 ClazzHasStudentDeleteException
package com.example.tlias_web_management.exception;public class ClazzHasStudentDeleteException extends RuntimeException {public ClazzHasStudentDeleteException() {}public ClazzHasStudentDeleteException(String message) {super(message);}
}
-
在全局处理器中添加异常处理方法
/** 处理违反"被删除班级下有关联的学生时, 不能直接删除"的异常*/ @ExceptionHandler public Result handleClazzHasStudentDeleteException(ClazzHasStudentDeleteException e) {log.error("程序出错", e);return Result.error("班级下有学员, 不能直接删除"); }
-
Controller
/** 删除班级* DELETE* http://localhost:8080/clazzs/5*/ @DeleteMapping("/{id}") public Result delete(@PathVariable Integer id) {log.info("删除班级: {}", id);clazzService.delete(id);return Result.success(); }
-
Service
- 先统计关联的学生数量, 如果学生数量不为 0, 抛出异常让全局异常处理器处理
@Override public void delete(Integer id) {Integer countOfStudents = studentMapper.countStudentsOfClazzByClazzId(id);if (countOfStudents != 0) {throw new ClazzHasStudentDeleteException();}clazzMapper.deleteById(id); }
-
Mapper
- StudentMapper
/** 根据 ID 查询班级关联的学生数量*/ @Select("select count(*) from student where clazz_id = #{clazzId}") Integer countStudentsOfClazzByClazzId(Integer clazzId);
- ClazzMapper
/** 根据 ID 删除班级*/ @Delete("delete from clazz where id = #{id}") void deleteById(Integer id);
Apifox 测试
-
测试异常情况
-
测试正常情况
学生管理
查询所有班级
略
列表查询
需求
- 需要查询 clazzName
接口开发
- StudentQueryParam
package com.example.tlias_web_management.pojo;import lombok.Data;/** 学生条件分页查询参数封装类*/
@Data
public class StudentQueryParam {private String name;private Integer degree;private Integer clazzId;private Integer page = 1;private Integer pageSize = 10;
}
- Controller
package com.example.tlias_web_management.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.pojo.Result;
import com.example.tlias_web_management.pojo.Student;
import com.example.tlias_web_management.pojo.StudentQueryParam;
import com.example.tlias_web_management.service.StudentService;import lombok.extern.slf4j.Slf4j;@Slf4j
@RequestMapping("/students")
@RestController
public class StudentController {@Autowiredprivate StudentService studentService;/** 条件分页查询* GET* http://localhost:8080/students?name=张三°ree=1&clazzId=2&page=1&pageSize=5*/@GetMappingpublic Result page(StudentQueryParam studentQueryParam) {log.info("学生条件查询参数: {}", studentQueryParam);PageResult<Student> pageResult = studentService.page(studentQueryParam);return Result.success(pageResult);}
}
- Service
package com.example.tlias_web_management.service.impl;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import com.example.tlias_web_management.mapper.StudentMapper;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.pojo.Student;
import com.example.tlias_web_management.pojo.StudentQueryParam;
import com.example.tlias_web_management.service.StudentService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;@Service
public class StudentServiceImpl implements StudentService {@Autowiredprivate StudentMapper studentMapper;@Overridepublic PageResult<Student> page(StudentQueryParam studentQueryParam) {PageHelper.startPage(studentQueryParam.getPage(), studentQueryParam.getPageSize());List<Student> studentList = studentMapper.list(studentQueryParam);Page<Student> p = (Page<Student>) studentList;return new PageResult<Student>(p.getTotal(), p.getResult());}
}
- Mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.tlias_web_management.mapper.StudentMapper"><!-- resultType: 查询返回的单条记录的类型 --><!-- list --><select id="list" resultType="com.example.tlias_web_management.pojo.Student">select s.*, c.name clazzName from student s left join clazz c on s.clazz_id = c.id<where><if test="name != null and name != ''">s.name like concat('%', #{name}, '%')</if><if test="degree != null">and s.degree = #{degree}</if><if test="clazzId != null">and s.clazz_id = #{clazzId}</if></where>order by c.update_time desc</select>
</mapper>
Apifox 测试
新增学生
需求
接口开发
-
Controller
/** 新增学生* POST* http://localhost:8080/students 请求体*/ @PostMapping public Result add(@RequestBody Student student) {log.info("新增学生: {}", student);studentService.save(student);return Result.success(); }
-
Service
@Override public void save(Student student) {// 补全 createTime, updateTimestudent.setCreateTime(LocalDateTime.now());student.setUpdateTime(LocalDateTime.now());studentMapper.insert(student); }
-
Mapper
/** 新增学生*/ @Insert("insert into student (name, no, gender, phone, degree, clazz_id, id_card, is_college, address, graduation_date, create_time, update_time) " + "values (#{name}, #{no}, #{gender}, #{phone}, #{degree}, #{clazzId}, #{idCard}, #{isCollege}, #{address}, #{graduationDate}, #{createTime}, #{updateTime})") void insert(Student student);
Apifox 测试
根据 ID 查询学生
需求
接口开发
略
Apifox 测试
修改学生
需求
接口开发
略
Apifox 测试
删除学生
需求
- 注意要批量删除
接口开发
-
Controller
- 注意是路径参数
/** 批量删除学生* DELETE* http://localhost:8080/students/1,2,3*/ @DeleteMapping("/{ids}") public Result delete(@PathVariable List<Integer> ids) {log.info("批量删除学生: {}", ids);studentService.deleteByIds(ids);return Result.success(); }
-
Service
@Override public void deleteByIds(List<Integer> ids) {studentMapper.deleteByIds(ids); }
-
Mapper
<delete id="deleteByIds">delete from student where id in<foreach collection="ids" item="id" open="(" close=")" separator=",">#{id}</foreach> </delete>
Apifox 测试
违纪处理
需求
接口开发
-
Controller
/** 违纪处理* PUT* http://localhost:8080/students/violation/5/5*/ @PutMapping("violation/{id}/{score}") public Result updateViolationData(@PathVariable Integer id, @PathVariable Integer score) {log.info("学生违纪处理, id: {}, score: {}", id, score);studentService.updateViolationDataById(id, score);return Result.success(); }
-
Service
- 这个异常处理好像没什么必要
@Override public void updateViolationDataById(Integer id, Integer score) {if (score == null) {throw new IllegalArgumentException("score cannot be null.");}Student student = studentMapper.getById(id);Short violationCount = student.getViolationCount();if (violationCount == null) {violationCount = 0;}student.setViolationCount((short) (violationCount + 1));Short violationScore = student.getViolationScore();if (violationScore == null) {violationScore = 0;}student.setViolationScore((short) (violationScore + score));student.setUpdateTime(LocalDateTime.now());studentMapper.updateById(student); }
-
Mapper
- 使用之前已经实现的 updateById
-
GlobalExceptionHandler (好像没什么必要)
/** 处理"参数不合法"的异常*/ @ExceptionHandler public Result handleIllegalArgumentException(IllegalArgumentException e) {log.error("程序出错", e);return Result.error(e.getMessage()); }
Apifox 测试
学生信息统计
班级人数统计
需求
接口开发
- 实体类 StudentCount 封装结果
package com.example.tlias_web_management.pojo;import java.util.List;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class StudentCount {private List<Object> clazzList;private List<Object> dataList;
}
-
Controller
/** 班级人数统计* GET* http://localhost:8080/report/studentCountData*/ @GetMapping("/studentCountData") public Result getStudentCountData() {log.info("统计班级人数");StudentCount studentCount = reportService.getStudentCountData();return Result.success(studentCount); }
-
Service
@Override public StudentCount getStudentCountData() {List<Map<String, Object> > list = studentMapper.countStudentClazzData();List<Object> clazzList = list.stream().map(dataMap -> dataMap.get("clazz")).toList();List<Object> dataList = list.stream().map(dataMap -> dataMap.get("total")).toList();return new StudentCount(clazzList, dataList); }
-
Mapper
- StudentMapper
/** 班级人数统计*/ @MapKey("clazz") List<Map<String, Object> > countStudentClazzData();
- xml
<select id="countStudentClazzData" resultType="java.util.Map">select if(c.name is null, '未知班级', c.name) clazz, count(*) total from student s left join clazz c on s.clazz_id = c.idgroup by if(c.name is null, '未知班级', c.name) order by total desc </select>
Apifox 测试
学生学历统计
需求
接口开发
-
Controller
/** 学生学历统计* GET* http://localhost:8080/report/studentDegreeData*/ @GetMapping("/studentDegreeData") public Result getStudentDegreeData() {log.info("统计学生学历信息");List<Map<String, Object> > degreeList = reportService.getStudentDegreeData();return Result.success(degreeList); }
-
Service
@Override public List<Map<String, Object> > getStudentDegreeData() {return studentMapper.countStudentDegreeData(); }
-
Mapper
- StudentMapper
/** 学生学历统计*/ @MapKey("name") List<Map<String, Object> > countStudentDegreeData();
- xml
<select id="countStudentDegreeData" resultType="java.util.Map">select(case degreewhen 1 then '初中'when 2 then '高中'when 3 then '大专'when 4 then '本科'when 5 then '硕士'when 6 then '博士'else '未知学历'end) name,count(*) valuefrom student group by degree order by degree </select>
Apifox 测试
功能完善
需求
如果部门下有员工, 不能删除部门, 并返回错误信息: 对不起, 当前部门下有员工, 不能直接删除
接口开发
班级下有学生, 也不能直接删除班级, 这一点之前已经实现了, 尝试统一这两个需求
- 自定义异常 AssociatedObjectsExistException
package com.example.tlias_web_management.exception;public class AssociatedObjectsExistException extends RuntimeException {private String objectName;private String associatedObjectName;public AssociatedObjectsExistException(String objectName, String associatedObjectName) {super("对不起, 当前" + objectName + "下有" + associatedObjectName + ", 不能直接删除!");this.objectName = objectName;this.associatedObjectName = associatedObjectName;}public String getObjectName() {return objectName;}public String getAssociatedObjectName() {return associatedObjectName;}
}
-
全局异常处理器中实现异常处理方法
/** 处理违反"被删除对象下有关联的对象时, 不能直接删除"的异常*/ @ExceptionHandler public Result handleAssociatedObjectsExistException(AssociatedObjectsExistException e) {log.error("程序出错", e);return Result.error(e.getMessage()); }
-
在三层架构中, 两者实现基本一致, 以删除部门为例
- Controller
/** 根据 ID 删除部门* DELETE* http://localhost:8080/depts/1*/ @DeleteMapping("/{id}") public Result delete(@PathVariable Integer id) {log.info("根据 ID 删除部门, id: {}", id);deptService.deleteById(id);return Result.success(); }
- Service
@Override public void deleteById(Integer id) {Integer countOfEmps = empMapper.countEmpsOfDeptByDeptId(id);if (countOfEmps != 0) {throw new AssociatedObjectsExistException("部门", "员工");}deptMapper.deleteById(id); }
- 需要统计关联的员工数量, 因此调用 empMapper
/** 根据 ID 查询部门关联的员工数量*/ @Select("select count(*) from emp where dept_id = #{deptId}") public Integer countEmpsOfDeptByDeptId(Integer deptId);
Apifox 测试
-
测试删除班级异常情况
-
测试删除班级正常情况
-
测试删除部门异常情况
-
测试删除部门正常情况
前后端联调测试
- 测试删除部门异常情况
备注
暂未修改: 一些接口返回给前端的数据中比接口文档要求的多了几个数据, 比如 deptName, clazzName, masterName
登录认证
登录功能
- LoginInfo 封装返回的登录信息
package com.example.tlias_web_management.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {private Integer id;private String username;private String name;private String token;
}
- Controller
package com.example.tlias_web_management.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;import com.example.tlias_web_management.pojo.Emp;
import com.example.tlias_web_management.pojo.LoginInfo;
import com.example.tlias_web_management.pojo.Result;
import com.example.tlias_web_management.service.EmpService;import lombok.extern.slf4j.Slf4j;@Slf4j
@RestController
public class LoginController {@Autowiredprivate EmpService empService;/** 员工登录* POST* http://localhost:8080/login*/@PostMapping("/login")public Result login(@RequestBody Emp emp) {log.info("员工请求登录: {}", emp);LoginInfo loginInfo = empService.login(emp);if (loginInfo != null) {return Result.success(loginInfo);}return Result.error("用户名或密码错误");}
}
-
Service
- 注意: 此时没有为 token 赋值
@Override public LoginInfo login(Emp emp) {Emp empLogin = empMapper.getInfoByUsernameAndPassword(emp);if (empLogin != null) {return new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), null);}return null; }
-
Mapper
/** 根据用户名和密码查询员工*/ @Select("select * from emp where username = #{username} and password = #{password}") public Emp getInfoByUsernameAndPassword(Emp emp);
-
Apifox 测试
但是, 此时实现的登录功能, 即便不登录, 直接访问 http://localhost:90, 也可以进入系统页面; 而真正的登录功能, 应当是只有登录后才能访问系统, 不登录则跳转到登录页面
问题在于: 此前我们实现的接口, 在服务端没有判断是否登录, 因此需要登录校验
登录校验
会话技术
Cookie
- 代码测试
@Slf4j
@RestController
public class SessionController {//设置Cookie@GetMapping("/c1")public Result cookie1(HttpServletResponse response){response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookiereturn 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();}
}
Session
- 代码测试
@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);}
}
token
JWT 令牌
登录时下发令牌
- JwtUtils 工具类: 注意
parseClaimsJws
不要混为parseClaimsJwt
了! 否则之后使用拦截器有 bug
package com.example.tlias_web_management.utils;import java.sql.Date;
import java.util.Map;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;public class JwtUtils {private static String signKey = "SVRIRUlNQQ==";private static Long expire = 43200000L;/** 生成 JWT 令牌*/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 令牌*/public static Claims parseJWT(String jwt) {Claims claims = Jwts.parser().setSigningKey(signKey).parseClaimsJws(jwt).getBody();return claims;}
}
-
完善 EmpServiceImpl
@Override public LoginInfo login(Emp emp) {Emp empLogin = empMapper.getInfoByUsernameAndPassword(emp);if (empLogin != null) {Map<String, Object> dataMap = new HashMap<>();dataMap.put("id", empLogin.getId());dataMap.put("username", empLogin.getUsername());String jwt = JwtUtils.generateJwt(dataMap);return new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), jwt);}return null; }
-
Apifox
拦截器 Interceptor
令牌校验
- 自定义拦截器 TokenInterceptor
package com.example.tlias_web_management.interceptor;import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;import com.example.tlias_web_management.utils.JwtUtils;import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求 urlString url = request.getRequestURL().toString();// 判断 url 是否含 login, 如果包含说明是登录操作, 放行if (url.contains("login")) {log.info("登录请求, 放行");return true;}// 获取请求 tokenString jwt = request.getHeader("token");// 判断 token 是否存在, 如果不存在, 返回错误结果 (未登录)if (!StringUtils.hasLength(jwt)) {log.info("获取到 jwt 令牌为空, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return false;}// 解析 token, 如果解析失败, 返回错误结果 (未登录)try {JwtUtils.parseJWT(jwt);} catch (Exception e) {e.printStackTrace();log.info("令牌解析失败, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return false;}log.info("令牌合法, 放行");return true;}
}
- WebConfig 中配置拦截器
package com.example.tlias_web_management.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import com.example.tlias_web_management.interceptor.TokenInterceptor;@Configuration
public class WebConfig implements WebMvcConfigurer {// 拦截器对象@Autowiredprivate TokenInterceptor tokenInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册自定义拦截器对象registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");}
}
- Apifox 测试
拦截路径
执行流程
AOP
AOP: 面向切面编程, 可以理解为面向特定方法编程
比如要得到每个方法的运行时间: 如果不使用 AOP, 则需要在每一个方法中添加几行代码; 如果使用 AOP, 可以单独定义一段代码, 而无需在业务方法中添加大量的重复性的代码
因此, AOP 优势在于:
- 减少重复代码: 因为已经将重复性的代码抽取到了 AOP 程序中
- 代码无侵入: 基于 AOP 实现时, 不需要修改原有的业务代码
- 提高开发效率
- 维护方便
入门程序
- 引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 定义切面类
package com.example.tlias_web_management.aop;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;import lombok.extern.slf4j.Slf4j;@Aspect // 当前类是切面类
@Slf4j
@Component
public class RecordTimeAspect {@Around("execution(* com.example.tlias_web_management.service.impl.*.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {long begin = System.currentTimeMillis();Object result = pjp.proceed();long end = System.currentTimeMillis();log.info("方法执行耗时: {} ms", end - begin);return result;}}
- 理解
- 连接点 JoinPoint, 可以被 AOP 控制的方法 (暗含方法执行时的相关信息)
- 通知 Advice, 指那些重复的逻辑, 共性功能 (最终体现为一个方法
recordTime
) - 切入点 PointCut, 匹配连接点的条件, 通知仅会在切入点方法执行时被应用
@Around("execution(* com.example.tlias_web_management.service.impl.*.*(..))")
- 切面 Aspect, 描述通知与切入点的对应关系 (通知 + 切入点)
- 目标对象 Target, 通知所应用的对象
通知类型
-
@PointCut
将公共的切入点表达式抽取出来- 切入点方法
@PointCut("execution(* com.example.tlias_web_management.service.*.*(..))") private void pt() {}
- 引入切入点
@Before("pt()") public void before(JoinPoint joinPoint) {log.info("before ..."); }
- 如果
pt()
使用 private 修饰, 只能在当前切面类引入该表达式; 如果改成 public, 可以在其他切面类引入
@Before("com.example.tlias_web_management.aop.RecordTimeAspect.pt()")
通知顺序
多个通知方法的执行顺序: 不同切面类中, 默认按照切面类的类名字母排序
- 目标方法前的通知方法: 字母排序靠前的先执行
- 目标方法后的通知方法: 字母排序靠前的后执行
可以使用 @Order
控制通知的执行顺序 (声明在切面类上)
- 前置通知: 数字越小越先执行
- 后置通知: 数字越小越后执行
切入点表达式
execution
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)// ? 前是可以省略的部分
- 通配符
@annotation
如果要匹配多个无规则的方法, 使用 execution 描述不方便, 此时使用 @annotation
简化切入点的书写
- 编写自定义注释
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {}
- 在业务类中要作为连接点的方法上添加自定义注解
@Override
@LogOperation // 自定义注解, 表示当前方法属于目标方法
public List<Dept> list() {}
- 切入类
@Before("@annotation(com.itheima.anno.LogOperation)")
public void before() {}
日志管理
需求
分析
记录日志
- 创建数据库表
-- 操作日志表
create table operate_log(id int unsigned primary key auto_increment comment 'ID',operate_emp_id 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 '返回值, 存储json格式',cost_time int comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
- 实体类 OperateLog
package com.example.tlias_web_management.pojo;import java.time.LocalDateTime;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {private Integer id;private Integer operateEmpId;private LocalDateTime operateTime;private String className;private String methodName;private String methodParams;private String returnValue;private Long costTime;
}
- 自定义注解 @LogOperation
package com.example.tlias_web_management.anno;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/** 自定义注解, 用于标识哪些方法需要记录日志*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {}
- OperateLogMapper
package com.example.tlias_web_management.mapper;import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;import com.example.tlias_web_management.pojo.OperateLog;@Mapper
public interface OperateLogMapper {/** 插入日志数据*/@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +"values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime})")public void insert(OperateLog log);}
-
如何获取执行方法的员工 ID? 在一个地方存储员工 ID: ThreadLocal
- 定义 ThreadLocal 操作的工具类 CurrentHolder, 用于操作当前登录的员工 ID
package com.example.tlias_web_management.utils;public class CurrentHolder {private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();public static void setCurrentId(Integer empId) {CURRENT_LOCAL.set(empId);}public static Integer getCurrentId() {return CURRENT_LOCAL.get();}public static void remove() {CURRENT_LOCAL.remove();} }
- 修改 TokenInterceptor
package com.example.tlias_web_management.interceptor;import org.apache.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor;import com.example.tlias_web_management.utils.CurrentHolder; import com.example.tlias_web_management.utils.JwtUtils;import io.jsonwebtoken.Claims; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j;@Slf4j @Component public class TokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求 urlString url = request.getRequestURL().toString();// 判断 url 是否含 login, 如果包含说明是登录操作, 放行if (url.contains("login")) {log.info("登录请求, 放行");return true;}// 获取请求 tokenString jwt = request.getHeader("token");// 判断 token 是否存在, 如果不存在, 返回错误结果 (未登录)if (!StringUtils.hasLength(jwt)) {log.info("获取到 jwt 令牌为空, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return false;}// 解析 token, 如果解析失败, 返回错误结果 (未登录)try {Claims claims = JwtUtils.parseJWT(jwt);Integer empId = Integer.valueOf(claims.get("id").toString());log.info("当前线程绑定员工 ID: {}", empId);CurrentHolder.setCurrentId(empId); // 存入员工 ID} catch (Exception e) {e.printStackTrace();log.info("令牌解析失败, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return false;}log.info("令牌合法, 放行");return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {CurrentHolder.remove();log.info("方法执行完毕, 清空当前线程绑定的员工 ID");} }
-
切面类
package com.example.tlias_web_management.aop;import java.time.LocalDateTime;
import java.util.Arrays;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import com.example.tlias_web_management.anno.LogOperation;
import com.example.tlias_web_management.mapper.OperateLogMapper;
import com.example.tlias_web_management.pojo.OperateLog;
import com.example.tlias_web_management.utils.CurrentHolder;import lombok.extern.slf4j.Slf4j;@Aspect // 当前类是切面类
@Slf4j
@Component
public class OperationLogAspect {@Autowiredprivate OperateLogMapper operateLogMapper;@Around("@annotation(logOperation)")public Object around(ProceedingJoinPoint joinPoint, LogOperation logOperation) throws Throwable {long startTime = System.currentTimeMillis();Object result = joinPoint.proceed();long endTime = System.currentTimeMillis();long costTime = endTime - startTime;OperateLog operateLog = new OperateLog();operateLog.setOperateEmpId(CurrentHolder.getCurrentId());operateLog.setOperateTime(LocalDateTime.now());operateLog.setClassName(joinPoint.getTarget().getClass().getName());operateLog.setMethodName(joinPoint.getSignature().getName());operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));operateLog.setReturnValue(result.toString());operateLog.setCostTime(costTime);log.info("记录操作日志: {}", operateLog);operateLogMapper.insert(operateLog);return result;};}
-
记得在 Controller 层的增删改方法上添加注解
@LogOperation
-
注意不同类型的通知获取连接点信息使用的对象不同
日志列表查询
需求
接口开发
- 注意到前端页面中有操作人, 修改实体类 OperateLog
package com.example.tlias_web_management.pojo;import java.time.LocalDateTime;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {private Integer id;private Integer operateEmpId;private LocalDateTime operateTime;private String className;private String methodName;private String methodParams;private String returnValue;private Long costTime;private String operateEmpName;
}
- Controller
package com.example.tlias_web_management.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import com.example.tlias_web_management.pojo.OperateLog;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.pojo.Result;
import com.example.tlias_web_management.service.OperateLogService;import lombok.extern.slf4j.Slf4j;@Slf4j
@RequestMapping("/log")
@RestController
public class OperateLogController {@Autowiredprivate OperateLogService operateLogService;/** 日志列表查询* GET* http://localhost:8080/log/page?page=1&pageSize=10*/@GetMapping("/page")public Result page(@RequestParam Integer page, @RequestParam Integer pageSize) {log.info("日志列表查询, page: {}, pageSize: {}", page, pageSize);PageResult<OperateLog> pageResult = operateLogService.page(page, pageSize);return Result.success(pageResult);}
}
- Service
package com.example.tlias_web_management.service.impl;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import com.example.tlias_web_management.mapper.OperateLogMapper;
import com.example.tlias_web_management.pojo.OperateLog;
import com.example.tlias_web_management.pojo.PageResult;
import com.example.tlias_web_management.service.OperateLogService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;@Service
public class OperateLogServiceImpl implements OperateLogService {@Autowiredprivate OperateLogMapper operateLogMapper;@Overridepublic PageResult<OperateLog> page(Integer page, Integer pageSize) {PageHelper.startPage(page, pageSize);List<OperateLog> operateLogList = operateLogMapper.list();@SuppressWarnings("resource")Page<OperateLog> p = (Page<OperateLog>) operateLogList;return new PageResult<OperateLog>(p.getTotal(), p.getResult());}
}
- Mapper
package com.example.tlias_web_management.mapper;import java.util.List;import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;import com.example.tlias_web_management.pojo.OperateLog;@Mapper
public interface OperateLogMapper {/** 插入日志数据*/@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +"values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime})")public void insert(OperateLog log);/** 日志列表查询*/@Select("select o.*, e.name operateEmpName from operate_log o left join emp e on o.operate_emp_id = e.id")public List<OperateLog> list();}