【苍穹外卖笔记day02】第一节 -- 新增员工(超详细,包含知识点说明)
本文摘要:苍穹外卖项目第二天开发笔记,主要内容为员工管理与分类管理功能开发。重点记录了新增员工功能的实现过程,包括需求分析、接口设计、数据库表结构(employee表)以及代码实现步骤。通过EmployeeDTO实现前端数据封装,使用BeanUtils进行对象属性拷贝,完成员工信息的新增操作。涉及知识点:数据传输对象(DTO)转换、Spring BeanUtils工具类使用等。开发中采用分层架构设
苍穹外卖笔记day02
第二天 员工管理、分类管理
第一节 – 新增员工
一、需求分析和设计
- 产品原型

- 接口设计
管理端发出的请求,统一使用 **/admin **作为前缀
用户端发出的请求,统一使用 /user 作为前缀
3. 数据库表设计(employee表):
| 字段名 | 数据类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | 自增 |
| name | varchar(32) | 姓名 | |
| username | varchar(32) | 用户名 | 唯一 |
| password | varchar(64) | 密码 | |
| phone | varchar(11) | 手机号 | |
| sex | varchar(2) | 性别 | |
| id_number | varchar(18) | 身份证号 | |
| status | Int | 账号状态 | 1正常 0锁定 |
| create_time | Datetime | 创建时间 | |
| update_time | Datetime | 最后修改时间 | |
| create_user | bigint | 创建人id | |
| update_user | bigint | 最后修改人id |
二、代码开发
1. 根据新增员工接口设计对应的DTO:

注意: 当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装前端提交的对应数据,如下图所示:
2. 代码实现:新增员工
(1)在EmployeeController中创建新增员工方法,接收前端提交的参数:
EmployeeController.java
/**
* 新增员工方法
* @param employeeDTO
*/
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO) {
//@RequestBody 传递的是json格式的代码,要加这个注解
log.info("新增员工:{}", employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}
(2)Alt + Enter,选择Create method’save’ in ‘EmployeeService’

(3)在EmployeeService.java中添加方法
/**
* 新增员工方法
* @param employeeDTO
*/
void save(EmployeeDTO employeeDTO);

(4)在EmployeeServiceImpl实现类中,实现新增员工方法
在Mapper层持久层把数据插入,传进来是DTO(方便封装前端提交的数据),但最终传回持久层,建议使用实体类。也就是说需要做一个实体的对象转换。
/**
* 新增员工方法
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
//DTO转换为实体
Employee employee = new Employee();
// 手写传递:把一个个属性都传递过去 ,繁琐
//employee.setName(employeeDTO.getName());
//对象的属性拷贝,一行代码(属性名必须一致)
BeanUtils.copyProperties(employeeDTO , employee);
//设置账号状态,默认正常(1表示正常,0表示锁定)
employee.setStatus(StatusConstant.ENABLE); //ENABlE启用=1,DISABLE禁用=0
//设置密码,默认密码123456 ,MD5加密
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
// 设置当前记录创建人的id和修改人的id
// TODO 后期需要改为当前用户的id
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
//调用持久层
employeeMapper.insert(employee);
}

🙋学知识: DTO对象转换的方法 与 解释链接请看下面:
employeeDTO: 是“数据传输对象”,通常从前端或远程服务接收数据。
employee:是“领域实体/持久化对象”,最终要落库。
对象的转换:
1. 传统手动字段拷贝,原封不动直接塞:
写法:Employee e1 = new Employee(); e1.setName(dto.getName()); e1.setAge(dto.getAge()); ……2. 使用spring BeanUtils,对象的属性拷贝(浅拷贝):
(反射,同名字段自动拷)DTO属性和employee属性都对应得上, 一行代码拷贝所有同名字段。
写法:BeanUtils.copyProperties (源对象,目标对象)Employee e2 = new Employee(); BeanUtils.copyProperties(dto, e2);xml 依赖(Maven):
<!-- Spring BeanUtils --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.32</version> </dependency>3. 使用Lombok + Builder(无反射,字段少时最直观):
写法:Employee e3 = Employee.builder() .name(dto.getName()) .age(dto.getAge()) .build();xml 依赖(Maven):
<!-- Lombok (想用Builder) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency>总结:
字段少:直接手写或 Builder 最清晰。
字段多:用 BeanUtils 图省事,但反射性能略低。
代码好看:用Lombok +builder
高并发或复杂转换:上 MapStruct,一次编译终生“飞”。
(5)在持久层Mapper插入员工数据
把数据插入Mapper,Mapper执行sql EmployeeMapper.insert(emp)
↓ 1 MapperProxy 代理 → 2 SqlSession → 3 JDBC PreparedStatement ↓ 4 MySQL Server 解析 → 5InnoDB 写内存+Redo Log → 6 后台线程刷磁盘
/**
*插入员工数据
* @param employee
*/
//单表的插入工作(application.yml 支持驼峰命名,id_number就可以自动转换成idNumber)
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_usr, updata_user, status)" +
"values" +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
void insert(Employee employee);

🙋学知识: Web 请求处理链路 的 完整流程
浏览器发请求 → Controller 接收 DTO → ServiceImpl 负责 DTO→Entity 转换 & 业务实现 → Mapper 生成 SQL → MyBatis → JDBC → MySQL Server → 磁盘
面试:
“我们采用 DTO 模式 隔离对外契约,请求链路是:
Controller 接收 DTO → ServiceImpl (实现类)做转换 & 业务 → Mapper → SQL → JDBC → MySQL → 磁盘。”
- HTTP 进来
POST http://localhost:8080/employeeBody: { "name":"Alice","username":"ali", ... }- Controller 接收并调用 Service
- Controller层仅做参数接收 + 调用下一层
- Service接口,定义契约
- Service(接口)在这一套架构里只做一件事:定义契约,
(1) 对外(Controller)暴露稳定的方法签名
- 不管内部怎么换实现,Controller 只认接口,达到隔离变化。
(2) 对内(ServiceImpl)提供实现占位,方便:
- Spring 用 JDK 动态代理做 AOP(事务、日志、权限)
- 单元测试时mock 接口即可,不用关心真实实现
(3) 本身不含任何业务代码或转换逻辑;所有具体工作交给 ServiceImpl 完成
Service = 门面 + 契约;ServiceImpl = 真正干活的
- ServiceImpl做业务 + 事务
- ServiceImpl = 参数校验 + 业务补全 + 事务控制 + 下发 SQL 的“一站式指挥官”。
- Mapper(DAO)接口 → MyBatis 代理
- 实际动作:MyBatis 生成 SQL → 送 JDBC
@Mapper public interface EmployeeMapper { @Insert("INSERT INTO employee(name,username,...) " + "VALUES(#{name},#{username},...)") void insert(Employee e); }
- JDBC → MySQL Server
- JDBC 把 SQL 通过网络发到 MySQL Server
- Server 解析 → 优化 → 执行 → 写 InnoDB Buffer Pool
- InnoDB → 磁盘
- 事务提交时 redo log 刷盘
- 后台线程把脏页写入 .ibd 文件
- 最终数据保存在:datadir/数据库名/表名.ibd
三、功能测试
。
方式:
(1)通过接口文档测试
(2)通过前后端联调测试
注意: 由于开发阶段前端和后端是并存开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法前后端联调测试。所以在开发阶段后端测试主要以接口文档测试为主。
通过后端联调测试
-
debug运行项目
-
打开
localhost:8080/doc.html后端项目接口文档
→员工相关接口→POST 新增员工页面→点击调试 -
输入请求参数

-
添加一个断点

-
再回到后端接口文档,点击“发送按钮”。
发送后报401
-
报错原因:我们执行接口时,并没有把Jwt令牌提交过来。
此时我们在sky-server/src/main/java/com/sky/interceptor/JwtTokenAdminInterceptor.java中,添加一个断点,然后在后端接口文档中,再次点击“发送”按钮,运行下一步,我们发现,token为null,运行下一步,token如果为空,则抛出异常,响应码为401。
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
- 获取令牌
在员工共相关接口中→员工登录页面→调试→发送登录请求→即可正常获取到令牌
- 复制令牌,前往文档管理→全局参数设置中→添加参数按钮
参数名称必须输入token,参数值为刚刚我们复制的,点击确定
该操作表面:我们添加了一个全局的参数
- 我们回到新增员工接口文档,关闭打开的“新增员工”等标签页。
重新打开接口文档中的“POST 新增员工” 接口→调试→请求头部出现1个红色提示。
- 点击发送按钮,回到代码中看Jwt令牌,token不再是null值。

- 取消Jwt的断点,点击Resume Program放行,Controller层成功获取到前端发送的数据。并且已封装在了DTO里面。

- 找到ServiceImpl层的
save()方法,添加断点,然后点击放行,这时我们可以在save中看到已经获取到了。
- 单步运行,到创建对象,我们的employee对象还是空值。

再单步运行到对象属性拷贝,我们发现,employee的相关属性都有值了。
单步运行到调用持久层这一行,数据都已经准备好了,除了id
放行:可以看到发了一条insert插入语句,然后下面我们看到一条报错信息,不要怕,我们仔细看,column的create_usr属性没认出来,原因是在mapper中,我们少写了个e,补上create_user,还有修改updata_user为update_user。所以写这个的时候一定要小心的写,啊哈!!
改好后,重新运行, 接着在后端接口文档中,“新增员工”页面重新发送一下,然后放行调试,我们就可以成功新增一条数据了
- 验证是否成功,成功插入数据,最终返回
return Result.success();code为1,表示成功。
Result类的配置:成功就把code赋值为1
数据库刷新一下,就出现了我们刚刚增加的数据。
提问1:问什么这里新增有Jwt令牌验证,我都没有写在Controller等三层架构这种里面?
原因:因为在sky-server/src/main/java/com/sky/config/WebMvcConfiguration.java中我们注册了自定义拦截器,并且执行任何有/admin/**的都会被拦截做Jwt令牌校验,校验成功就会放行。
/**
* 把自定义的 JWT 拦截器注册到 SpringMVC 拦截器链。
* 任何匹配 "/admin/**" 的请求都会先被 jwtTokenAdminInterceptor 拦下做 JWT 校验;
* 登录接口 "/admin/employee/login" 直接放行,不做校验。
*
* @param registry Spring 提供的注册器,用来“挂”拦截器并指定拦截/放行规则
*/
@Override // 覆盖父类方法,Spring 启动时会自动回调
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器..."); // 日志:方便排查拦截器有没有被加载
registry
// 1. 把 JWT 拦截器实例挂到链里(jwtTokenAdminInterceptor 已提前 @Component 注入)
.addInterceptor(jwtTokenAdminInterceptor)
// 2. 拦截范围:所有 /admin/** 开头的后台接口(如 /admin/employee/page、/admin/category/list ...)
.addPathPatterns("/admin/**")
// 3. 白名单:以下路径不走拦截器(登录/退出等不需要 token 也能访问)
.excludePathPatterns("/admin/employee/login");
}

提问2:为什么WebMvcConfiguration.java会被执行
Spring Boot 会“自动”把 WebMvcConfiguration 扫描成配置类,原因如下:
- 你写了
@Configuration
告诉 Spring:这是一个“配置 Bean”,启动时要加载。 - 类实现了
WebMvcConfigurer(或继承WebMvcConfigurationSupport) - Spring MVC 启动时会自动检测所有实现该接口的 Bean,并调用其
addInterceptors(...)方法,从而把你的拦截器注册到全局拦截器链。
拦截器本身已经加了@Component(或@Autowired注入)
所以jwtTokenAdminInterceptor先被 Spring 扫描成 Bean,再在这里被引用。
总结
Spring Boot 启动 → 扫描到 @Configuration 类 → 发现它实现了 WebMvcConfigurer → 回调addInterceptors()→ 把 jwtTokenAdminInterceptor 注册进拦截器链 → 此后所有匹配 /admin/** 的请求都会先执行拦截器里的 preHandle() 方法。
前后端联调测试
- 输入前端登录地址:我的是http://mysky:8083/#/login,再输入账号密码进行登录操作。
- 点击员工管理→添加员工按钮

- 录入员工数据后保存

- 验证


四、 代码完善
程序存在的问题
1、录入的用户已存在,抛出异常后没处理
如果用户已存在,再次新增,代码会报500 
并且后台提示Duplicate entry ‘zhangsan’ for key ‘idx_username’,用户名已经存在了
原因:username添加了唯一约束Unique索引。
(1) 捕获sql异常
复制SQLIntegrityConstraintViolationException,把这个异常在sky-server/src/main/java/com/sky/handler/GlobalExceptionHandler.java中
重写一个处理异常的方法,来捕获异常,再返回设置的异常信息。
/**
* 统一处理数据库 SQL 完整性约束异常(如唯一索引冲突)。
* 目前主要场景:注册用户时用户名已存在,MySQL 会抛出
* SQLIntegrityConstraintViolationException,其错误信息格式为
* "Duplicate entry 'xxx' for key 'idx_xxx'"。
*
* 本方法把原始英文异常解析成用户友好的中文提示,并返回统一响应对象 Result。
*
* @param ex 数据库抛出的 SQLIntegrityConstraintViolationException
* @return 错误响应 Result,提示"用户名已存在"或"未知错误"
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
// 从异常消息中提取完整错误描述,例如:
// "Duplicate entry 'zhangsan' for key 'idx_username'"
String message = ex.getMessage();
// 仅处理"唯一索引冲突"场景
if (message.contains("Duplicate entry")) {
// 按空格拆分,第 3 段即为冲突值(单引号包裹)
String[] split = message.split(" ");
String username = split[2]; // 'zhangsan'
// 去掉首尾单引号,再拼接中文"已存在"
String msg = username.replace("'", "") + MessageConstant.ALREADY_EXISTS;
log.error("异常信息:{}", msg); //控制台也提示异常信息
//前端返回异常信息
return Result.error(msg);
}
// 其他未知约束异常,统一返回"未知错误"
return Result.error(MessageConstant.UNKNOWN_ERROR);
}

测试:
重新启动项目,debug,清空一下控制台,回到苍穹外卖项目接口文档,再新增员工出再点击一下发送,回到控制台,查看处理异常的信息,响应码200,并给出异常信息。

问题3:为什么GlovalExceptionHandler会抓取到业务异常
原因:GlobalExceptionHandler 是 Spring 提供的“异常切面”,只要加 @RestControllerAdvice + @ExceptionHandler,所有 Controller 抛出的指定异常都会先被它截获,统一日志、统一提示、统一返回,让异常处理不再散落各处。
知识点: (业务层 抛出的自定义异常BaseException)
SpringMVC 的异常处理时机 请求进入 Controller之后,任何环节(Controller、Service、Mapper) 抛出的异常,只要没在被调用栈里被 try-catch 掉,就会一路冒到 DispatcherServlet; 此时 Spring 发现你有@RestControllerAdvice + @ExceptionHandler(BaseException.class),就把异常对象传给你写的exceptionHandler(BaseException ex)方法。
/**
* 统一处理 业务层 抛出的自定义异常。
* 项目代码中凡是继承 {@link BaseException} 的异常(如账号已锁定、库存不足等)
* 都会被此方法捕获,日志记录错误描述,并返回统一 JSON 结果给前端。
*
* @param ex 继承 BaseException 的业务异常对象
* @return 统一响应体 Result,前端可直接取 message 字段提示用户
*/
@ExceptionHandler(BaseException.class) // 只抓 BaseException 及其子类,可把换成其他处理器
public Result exceptionHandler(BaseException ex) {
// 记录业务异常描述(不打印整个堆栈,避免日志膨胀)
log.error("业务异常:{}", ex.getMessage());
// 把异常描述原样返回,前端弹窗/文案即可使用
return Result.error(ex.getMessage());
}
###### 问题4:有了BaseException处理器捕获异常,为什么还要写SQLintegrityConstraintViolationException的处理器
理由:BaseException只能截住我们自己抛的业务异常;SQLIntegrityConstraintViolationException 是数据库抛的底层异常,不会被 BaseException 处理器 抓到,必须单独声明,否则用户会看到 500 + 英文堆栈。BaseException和SQLIntegrityConstraintViolationException ,两者完全独立,各管各的异常来源。
2、新增员工时,创建人id和修改人id设置为了固定值。

基于Jwt认证流程(新增员工,给创建者赋予id)

登录成功生成Jwt令牌:
员工登录EmployeeController做的事情,员工登录成功后会生成JWT令牌并响应给前端,文件位置:sky-server/src/main/java/com/sky/controller/admin/EmployeeController.java
/**
* 员工登录入口
* 路径:POST /admin/employee/login
* 前端传入:{"username":"admin","password":"123456"}
* 后端返回:{"code":1,"msg":"success","data":{"id":1,"userName":"admin","name":"管理员","token":"eyJxxx..."}}
*/
@PostMapping("/login") // 只接收 POST 请求
@ApiOperation(value = "员工登录") // Knife4j/Swagger 文档注释
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO); // 打印入参,方便排查
/* 1. 业务层校验账号+密码;失败抛 BaseException,由 GlobalExceptionHandler 统一返回错误信息 */
Employee employee = employeeService.login(employeeLoginDTO);
/* 2. 登录成功,开始生成 JWT ------------------------------------------- */
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); // 把员工主键存进 JWT 负载
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(), // 签名密钥(application.yml 里配置)
jwtProperties.getAdminTtl(), // 过期时间(默认 2 小时)
claims); // 自定义声明
/* 3. 组装返回给前端的 VO --------------------------------------------- */
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token) // 前端后续请求带此 token 放在 header
.build();
/* 4. 统一包装返回给前端 ---------------------------------------------------- */
return Result.success(employeeLoginVO); // {code:1,msg:success,data:...}
}
后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前员工id:
文件位置:sky-server/src/main/java/com/sky/interceptor/JwtTokenAdminInterceptor.java
问题:解析出登录员工ID后,如何传递给Service的save方法?
// ============== 统一 JWT 登录校验拦截器(preHandle)==============
// 1.从请求头中获取令牌: 从请求头里取出前端带过来的 token(header 名称在 yml 配置,默认 "token")
String token = request.getHeader(jwtProperties.getAdminTokenName());
// 2. 校验令牌:开始验 token —— 任何解析失败都抛异常,进入 catch 块
try {
log.info("jwt校验:{}", token); // 打印日志,方便排查
// 2-1 用**签名密钥**解析 JWT,若过期、被篡改、格式错误都会抛异常
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminTokenKey(), token);
// 2-2 取出登录时存进去的员工 id(key = "empId")
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:{}", empId); // 记录当前操作用户
//BaseContext.setCurrentId(empId); // 原码这里还没写:放进线程变量,后续 Service/Mapper 随时拿
// 2-3 全部正常 → 放行,继续进入 Controller
return true;
} catch (Exception ex) {
// 3. 解析失败(无 token、过期、篡改)→ 返回 401,前端收到未授权
response.setStatus(401);
return false; // 中断请求链,不再进入 Controller
}
🙋学习知识:JWT令牌【生成→传输→获取】全流程一览表
| 阶段 | 位置 | 关键代码/动作 | 数据形态 | 说明 |
|---|---|---|---|---|
| 1.登录校验 | 后端Controller | employeeService.login(dto) |
Java对象 | 校验账号密码,失败抛业务异常 |
| 2.生成令牌 | 后端Controller | JwtUtil.createJWT(secret,ttl,claims) |
字符串JWT | 把员工id写进claims并签名 |
| 3.返回前端 | 后端Controller | Result.success(vo) |
JSON响应体 | SpringMVC自动把VO→JSON |
| 4.前端保存 | 前端浏览器 | localStorage.setItem('token',res.data.token) |
字符串 | 完整JWT串存本地 |
| 5.请求携带 | 前端Axios拦截器 | axios.defaults.headers['token'] = 本地值 |
HTTPHeader | 每次请求自动带此头 |
| 6.后端获取 | 后端拦截器 | request.getHeader(jwtProperties.getAdminTokenName()) |
字符串 | 按配置名"token"取出整串JWT |
| 7.后端解析 | 后端拦截器 | JwtUtil.parseJWT(secret,token) |
Claims对象 | 验签、过期、篡改 |
| 8.取出员工id | 后端拦截器 | Long empId =claims.get(...) |
Long | 得到当前登录人主键 |
| 9.线程保存 | 后端拦截器 | BaseContext.setCurrentId(empId) |
ThreadLocal | 后续Service/Mapper随时取 |
| 10.放行 | 后端拦截器 | return true |
布尔 | 令牌合法,请求进入Controller |
现在代码里面没有写第9个线程保护:
- 必须写吗?
不是语法必须,但项目规范里“强烈建议写”。
不写也能跑,只是后续代码想拿当前登录人 ID 时得自己再解析一次 JWT,或者层层传参。- 为什么我们代码里现在没有?
我们那段拦截器那里现在还没写,还没写而已,实现自动填充人是要写的。
把BaseContext.setCurrentId(mpId);补上即可实现自动填充人这种。- 线程保护(ThreadLocal)作用
- 一次解析,全线程复用
- 避免在 Service/Mapper 里反复解析 JWT
- 避免层层方法显式传递 empId 参数
- 同一次请求内随时:
只有当你需要在 Service/Mapper 里知道“当前是谁”时才写。Long loginUserId = BaseContext.getCurrentId(); // 拿到当前登录员工主键
自动填充人;public void saveCategory(CategoryDTO dto) { Long loginUserId = BaseContext.getCurrentId(); // 拿到当前登录员工主键 Category category = new Category(); category.setName(dto.getName()); category.setCreateUser(loginUserId); // 自动填创建人 categoryMapper.insert(category); }
- 请求结束线程归还池子,ThreadLocal 被清理,数据不会串请求(我们项目没写,强烈建议添加)
(1) 现在没有 afterCompletion
→ 线程池复用线程时 ThreadLocal 里仍残留上一次请求的 empId,下次请求可能串数据(看起来就像“用户A突然变成用户B”)。
(2))补上只需 3 行代码,放在类里即可:
位置在:sky-server/src/main/java/com/sky/interceptor/JwtTokenAdminInterceptor.java/** 完整模板:*/ public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object >handler) throws Exception { ... // 解析、setCurrentId、return true/false } //加上这三行即可 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object >handler, Exception ex) throws Exception { // 请求结束,清空 ThreadLocal,防止线程复用串数据 BaseContext.removeCurrentId(); } }
代码完善(解决自动获取当前用户ID)
看了我的学习知识,在这里就不会迷糊
继续了解:
ThreadLocal 并不是一个Thread,而是Thread的局部变量。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
验证:
-
在EmployeeController.java的save方法中第一行,
-
在拦截器JwtTokenAdminInterceptor.java的preHandel方法中第一行,
-
在EmployeeServiceImpl.java中的save方法中,
都添加一句:System.out.println("当前线程的ID:" + Thread.currentThread().getId()); -
停止项目→debug重启项目→在前端重新登录账号→在新增员工处“新增已有账号laoliu”→点击保存

-
返回控制台观察,本次操作,线程id都相同

-
再回到页面,再次点击“保存”按钮,回到控制台查看:线程ID已改变,但是本次线程id依旧一一对应。

至此可以验证:ThreadLocal可以为每个线程提供单独一份存储空间,保证线程安全。
且也说明,在ThreadLocal的整个生命周期之内,我们就可以共享这一份存储空间。
而这,也就是说我们可以再拦截器中,把我们当前的用户ID存储到这个空间里面去。
再到Service层中,我们就可以从这个存储空间里面把用户ID拿出来。
具体实现
ThreadLocal常用方法:
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
- public void remove() 移除当前线程的线程局部变量
该ThreadLocal封装位置:sky-common/src/main/java/com/sky/context/BaseContext.java
package com.sky.context;
/**
* ThreadLocal 工具类
* 作用:在同一次请求线程内随处存/取/删除当前登录员工 ID,
* 避免层层传参,也防止线程复用时数据串号。
*/
public class BaseContext {
// 每个线程独享的变量,生命周期 = 请求开始到请求结束
private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 把当前登录员工主键保存到线程变量(一般在拦截器 preHandle 里调用)
* @param id 员工主键
*/
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
/**
* 从线程变量获取当前登录员工主键(Service/Mapper 随处调用)
* @return 员工主键;线程无值时返回 null
*/
public static Long getCurrentId() {
return threadLocal.get();
}
/**
* 请求结束务必调用,清理线程变量,防止线程池复用导致串号
* 建议在拦截器 afterCompletion 里使用
*/
public static void removeCurrentId() {
threadLocal.remove();
}
}
在程序当中使用,就只需要BaseContext.xxx就可以了
-
首先,在我们的JwtTokenAdminInterceptor.java中,我们这句
Long empId = - Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());已经解析出来empId; -
接下来,我们只需要在该句下面添加:
BaseContext.setCurrentId(empId);把这个当前用户ID存入进去。
-
程序继续执行→Ctroller→Service,最终执行到ServiceImpl的Sava方法中。
-
然后,把TODO完善:
// 设置当前记录创建人的id和修改人的id
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());

测试:(是否自动获取到id)
- 重新启动,debug,给JwtTokenAdminInterceptor.java和EmployeeServiceImpl.java都添加一个断点。

- 回到前端页面,添加员工,添加一个没有的账号,然后点击保存

- 程序运行到拦截器,点击单步运行Set Over,empId值已经解析出来了=1,然后再单步运行,BaseContext已经把empId存进去了

- 点击放行Resume Program,运行到EmployeeServiceImpl.java,我们想获取BaseContext.getCurrentId()的返回结果,我们可以选中它,然后右键,选择Evaluate Expression…,点击Evaluate,计算出结果value = 1,这个就是当前登录用户id。

Alt点击employee,也可以看到
- 放行Resume Program,看控制台, updates: 1,新增成功。

- navicat刷新一下,也看到了新增的王五的结果,create_user和update_user也都是1

7.至此已成功自动填充创建人,可以把//TODO删除了
提交代码
代码确认无误,点击Git→Conmmit,输入“新增员工业务代码开发”,再点击Commit and Push…
如果提交和推送后,弹出下面信息,不是失败,而是 IDE 在提交前做代码质量检查(Commit Checks)发现 62 个警告,让你先 Review。Review 就是让你“过一遍警告清单”,决定要不要修复。
觉得无关紧要 → 直接 Commit anyway
想保持代码整洁 → 逐条改掉再 commit
IDE 的 Review code analysis 会把所有 代码检查器(Inspection) 报出来的问题列成清单,常见类型如下:
| 类型 | 举例 | 是否必须修 |
|---|---|---|
| 缺失 JavaDoc 注释 | public void save() 缺少方法注释 |
否(warning) |
| 变量/导入未使用 | import java.util.List 从未使用 |
否(warning) |
| 潜在空指针 | String s = null; int len = s.length(); |
建议修(warning → 可能运行时异常) |
| 语法/编译错误 | 少分号、类型不匹配 | 必须修(error) |
| 代码风格 | 缩进、行长超过限制 | 否(warning) |
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)