Java Web - 项目

news/2025/2/28 1:35:44/文章来源:https://www.cnblogs.com/wxgmjfhy/p/18742430

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;
    }
    

部门管理

列表查询

需求

  • 由于部门数量较少, 不考虑分页显示
  • 对查询的结果, 根据最后的修改时间倒序排序
  • 接口文档

接口开发

三层架构:

  • 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();
      }
      
    • 调用 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
<?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>
      

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>
    

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=张三&degree=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, 也可以进入系统页面; 而真正的登录功能, 应当是只有登录后才能访问系统, 不登录则跳转到登录页面

问题在于: 此前我们实现的接口, 在服务端没有判断是否登录, 因此需要登录校验

登录校验

会话技术

  • 代码测试
@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 简化切入点的书写

  1. 编写自定义注释
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {}
  1. 在业务类中要作为连接点的方法上添加自定义注解
@Override
@LogOperation // 自定义注解, 表示当前方法属于目标方法
public List<Dept> list() {}
  1. 切入类
@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();}

前后端联调测试

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/890946.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux部署】Linux环境下Java项目Jar包的启动指令

在Java开发领域,我们经常需要将编译好的Java应用程序打包成Jar文件,以便于部署和运行。 特别是在Linux服务器上,管理多个Jar包的启动和停止是日常运维中的重要一环。 本文介绍如何在Linux环境下高效地启动和管理Jar包,同时提供简洁明了的代码示例,帮助大家更好地理解这一过…

手把手教你用 MicroPython 玩转幻尔串口舵机,代码+教程全公开

MicroPython串口舵机库,支持幻尔科技全系列舵机,支持mpremote工具一键导入,28条指令全测试。原文链接: FreakStudio的博客 摘要 MicroPython串口舵机库,支持幻尔科技全系列舵机,支持mpremote工具一键导入,28条指令全测试。 往期推荐: 学嵌入式的你,还不会面向对象??…

Plombery:将Python脚本的执行与Web界面的可视化监控完美结合的Python任务调度工具

还在为定时运行Python脚本而苦恼吗?还在为复杂的调度系统而头疼吗?今天,就让Plombery帮你解决这些问题!Plombery是一个简单易用的Python任务调度器,拥有友好的Web界面和REST API,让你轻松管理和监控你的Python脚本。告别复杂的配置和代码,Plombery将带你进入高效、便捷的…

AQS的acquire(int arg) 方法底层源码

一、定义 acquire(int arg) 是 AQS(AbstractQueuedSynchronizer)中的一个核心方法,用于在独占模式下获取同步状态。如果当前线程无法获取同步状态,则将其加入等待队列并阻塞,直到成功获取同步状态或被中断 1、acquire(int arg) 方法的作用功能:尝试获取同步状态(独占模式…

【钓鱼邮件】春节复工近期常见的钓鱼邮件

本期主要分享2025年2月常见的钓鱼邮件样本,特别提醒广大用户在春节复工高峰期加强安全防范。 补贴类钓鱼邮件 春节之后,五险一金补贴、年终奖补贴相关的钓鱼邮件依旧频发。钓鱼手法也有所提升,攻击者通常将通知内容放到附件中,并且对附件设置访问密码,试图绕过反垃圾系统检…

HTTP协议与RESTful API实战手册(终章):构建企业级API的九大秘籍

title: HTTP协议与RESTful API实战手册(终章):构建企业级API的九大秘籍 🔐 date: 2025/2/28 updated: 2025/2/28 author: cmdragon excerpt: 🏭 本文作为系列终章,通过物流管理系统的案例,揭秘API开发的完整流程。你将掌握: 深度解读28个HTTP协议进阶特性(ETag/CO…

第一周实验:二次开发

来源 来自大一舍友C++大作业。该项目模拟了一个图书管理系统,涉及到用户对于书籍的查看、借阅与归还,管理员对于书籍相关信息的增删改查。 运行环境+运行结果的截图 运行环境:Windows 11 + Visual Studio 2022main.cpp #include<Windows.h> #include "Account.h&…

学习笔记之day02 Linux-基础篇-系统安装

​1、操作系统简介操作系统:人与计算机硬件交互的中介Linux:内核+Shell +扩展软件Windows:内核+explorer.exe+软件类比法:计算机硬件 -- 内核 == 蛋黄 / Shell == 蛋清 / 外围应用程序 == 蛋壳常见的操作系统:Windows、Linux、DOS、UnixLinux操作系统开放源代码、可以自由…

绝缘电阻测试仪科普

什么是绝缘电阻绝缘电阻是指两个绝缘介质间的电阻,当另一端安装有电压源时,绝缘介质内电荷不能流动,因而受电压源作用,在另一端产生电势差,形成电阻抵消电压势差而不致使电荷漏出。一般情况下,绝缘电阻越大,电气设备的安全性就越好,缺陷率也越低。 为什么测量绝缘电阻绝…

【CodeForces训练记录】Educational Codeforces Round 175 (Rated for Div. 2)

训练情况赛后反思 CD连续卡题,D题树上层序遍历+加法原理,鉴定为基本的图论数据结构没学好 A题 直接打表,对于 i%3 = i%5 的情况,我们发现有三个一组,三个一组连续的数,每组第一个数之间差 15,所以我们 / 15 * 3 先把整组的数量算出来,再求是组内第几个,就能得到答案了…

软工五问

这个作业属于哪个课程 课程链接这个作业要求在哪里 作业要求这个作业的目标 学习使用markdown, 接触GitHub, 建立个人博客个人介绍 📋标签 广东湛江 人 期望成为 golang后端工程师 学习经历持续学习golang及其框架, 设计模式 持续学习后端各个组件的可靠高效解决方案兴趣爱好…

清华大学推出的5册免费的 DeepSeek 学习使用指南!

前言 在当今这个信息洪流、技术飞速迭代的时代,DeepSeek的横空出世极大地降低了普通人利用人工智能技术的门槛。然而,尽管机遇就在眼前,仍有不少朋友面对DeepSeek感到无从下手,不知如何利用它来紧握时代赋予的红利。对此,清华大学展现出了高度的社会责任感与前瞻性,推出了…