黑马程序员跟学JavaWeb开发教程P174员工表记录删除员工操作表记录正常写入解决方案
- 一、背景
- 二、具体代码
- 三、分析问题
- 四、总结问题
- 4.1 finally代码块的return语句影响最终执行结果
- 场景一、try代码块中有return,finally中无return,仅仅修改基本数据类型
- 场景二、try代码块中有return,finally中无return,仅仅修改引用类型
- 场景三、try代码块中有return,finally中无return,新建引用类型并修改
- 场景四、try代码块中有return,finally中也有return,仅仅修改基本数据类型
- 场景五、try代码块中有return,finally中也有return,仅仅修改引用类型
- 场景六、try代码块中有return,finally中也有return,新建引用类型并修改
- 场景七、try代码块中有异常和return,finally中也有return,仅仅修改基本数据类型 ☆
- 场景八、try代码块中有异常和return,finally中也有return,仅仅修改引用类型
- 场景九、try代码块中有异常和return,finally中也有return,新建引用类型并修改
- 场景十、finally代码一定会执行吗
- 五、finally总结
一、背景
原视频参考该链接
黑马程序员JavaWeb开发教程-P174-事务管理-事务进阶-propagation属性
跟着该链接学习的时候遇到了奇怪的问题,一时间没有解决,打算记录一下,巩固自身Java基础。
本文章核心知识点:1.finally代码块中不要跟return 2.propagation属性的用法
需求:点击删除按钮,删除掉该部门(表emp)
并且将该部门下的员工(表dept)都删除
最后将删除记录写入部门操作日志表(dept_log)
该课程主要练习的是propagation属性中的REQUIRES_NEW和REQUIRED
属性值 | 含义 |
---|---|
REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… |
下面通过我的文章带大家复盘一下我遇到的问题
二、具体代码
仿照视频中,我编写的代码如下
DeptServiceimpl.java类
package com.itcast.service.impl;
import com.itcast.mapper.DeptLogMapper;
import com.itcast.mapper.DeptMapper;
import com.itcast.mapper.EmpMapper;
import com.itcast.pojo.Dept;
import com.itcast.pojo.DeptLog;
import com.itcast.service.DeptLogService;
import com.itcast.service.DeptService;
import lombok.extern.slf4j.Slf4j;
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 java.time.LocalDateTime;
import java.util.List;
/**
* @ClassName: DeptServiceimpl
* @Description:
* @Author: xxx
* @Date: 2024/10/4
*/
@Service
@Slf4j
public class DeptServiceimpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;
/**
* @param id @return int
* @description // 该接口用于根据ID删除部门数据
* @date 2024/10/4 23:14
* @author xxx
**/
@Override
@Transactional(rollbackFor = Exception.class) // 当前方法交给Spring做事务管理
public int deleteDeptById(Integer id) throws Exception {
int countDeleteDept = 0;
try {
// 删除该部门下的所有员工
int countDeleteEmp = empMapper.deleteEmpByDeptId(id);
log.info("删除了id:" + id + "下的所有员工" + countDeleteEmp + "个");
// 异常处理
System.out.println(1 / 0);
/**if (true) {
throw new Exception("出错啦");
}*/
// 删除部门
countDeleteDept = deptMapper.deleteDeptById(id);
log.info("删除了id:" + id + "的部门" + countDeleteDept + "个");
} finally {
// 记录到部门日志表中
DeptLog deptLog = new DeptLog();
deptLog.setDeptId(id);
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("解散部门-部门ID:" + id);
deptLogService.saveDeptLog(deptLog);
log.info("插入了id:" + id + "的部门到日志表");
return countDeleteDept;
}
}
}
解析一下代码:
- deleteDeptById方法主要做了3件事。
- 1.是根据部门id(dept_id)先删除员工表emp中在该部门下的员工。
- 2.删除dept_id对应的dept表的部门。
- 3.将删除的动作记录日志。
- 通过
System.out.println(1 / 0);
手动触发算数异常,理想的情况是try代码块中的事务回退,finally中的代码跟着一起回退。实现的效果是,网页前端展示抛出异常,走全局异常处理器的逻辑。
如下图所示是视频中老师示例的理想结果,前端展示异常信息。
全局异常处理器代码如下:
package com.itcast.exception;
import com.itcast.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @ClassName: GlobalExceptionHandler
* @Description: 全局异常处理器
* @Author: xxx
* @Date: 2024/10/7
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* @param
* @return com.itcast.pojo.Result
* @description // 针对Exception异常进行捕获
* @param: e
* @date 2024/10/7 15:43
* @author xxx
**/
@ExceptionHandler(Exception.class)
public Result exception(Exception e) {
// 打印堆栈信息
e.printStackTrace();
return Result.fail("对不起,操作失败,请联系管理员");
}
}
而我实际的情况是,前端界面正常显示执行成功,算数异常前的事务(删除员工)执行成功,算数异常后的事务(删除部门)执行失败,日志表dept_log也正常写入。
我显示的界面如下:我点击删除“测试部门”
从请求中可以看到删除部门的状态码是200
IDEA控制台打印信息如下:没有rollback事件,只有1个commit事务
数据库中查看dept_log表正常写入
这个问题困扰了很久,查询了Chat-GPT,也在视频的弹幕中尝试了很多方法,,一时间都没能解决,之后自己琢磨出了答案。
三、分析问题
我的问题很明确,就是算数异常没有抛出,没有交给全局异常处理器管理。前台没有打印相应的报错信息。打断点在GlobalExceptionHandler类发现代码根本没有跑,于是我又一行一行跟着断点看了DeptServiceimpl.java的deleteDeptById方法。
发现原来是finally中跟了return,这是一个很容易犯错的地方。最后我将return语句从finally中拿了出来,正确代码如下:
@Override
@Transactional(rollbackFor = Exception.class) // 当前方法交给Spring做事务管理
public int deleteDeptById(Integer id) throws Exception {
int countDeleteDept = 0;
try {
// 删除该部门下的所有员工
int countDeleteEmp = empMapper.deleteEmpByDeptId(id);
log.info("删除了id:" + id + "下的所有员工" + countDeleteEmp + "个");
// 异常处理
System.out.println(1 / 0);
/*if (true) {
throw new Exception("出错啦");
}*/
// 删除部门
countDeleteDept = deptMapper.deleteDeptById(id);
log.info("删除了id:" + id + "的部门" + countDeleteDept + "个");
} finally {
// 记录到部门日志表中
DeptLog deptLog = new DeptLog();
deptLog.setDeptId(id);
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("解散部门-部门ID:" + id);
deptLogService.saveDeptLog(deptLog);
log.info("插入了id:" + id + "的部门到日志表");
}
// 注意这个return一定要放在finally外边,要不然全局异常处理器是触发不了的l
return countDeleteDept;
}
这样运行后达到了和视频中老师一样的效果。
员工表(emp)、部门表(dept)以及部门日志表(dept_log)均没有删除
IDEA控制台打印如下:Participating in existing transaction,意思是参与到一个已经存在的事务,也就是说写日志的操作和删除员工和部门的操作在一个事务中。
这依然不符合我们的最终需求,无论是否异常,记录部门操作日志的事务都要执行。那我们接着更改记录部门操作日志的业务层代码。
当前是这样:
package com.itcast.service.impl;
import com.itcast.mapper.DeptLogMapper;
import com.itcast.pojo.DeptLog;
import com.itcast.service.DeptLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @ClassName: DeptLogServiceImpl
* @Description:
* @Author: xxx
* @Date: 2024/10/7
*/
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
/**
* @param deptLog@return int
* @description // 保持信息到部门日志表
* @param: deptLog
* @date 2024/10/7 17:06
* @author xxx
**/
@Override
@Transactional(rollbackFor = Exception.class)
public int saveDeptLog(DeptLog deptLog) {
return deptLogMapper.saveDeptLog(deptLog);
}
}
我们在@Transactional注解中添加属性
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) // 需要新事务,无论有无,总是创建新事务
意思是在执行写入部门操作表的业务时,总是创建新事务。
REQUIRES_NEW :需要新事务,无论有无,总是创建新事务
这样最终实现了代码需求。
四、总结问题
4.1 finally代码块的return语句影响最终执行结果
该问题是我对finally的理解不到位,下面看几个例子,进一步巩固对finally和return的理解
场景一、try代码块中有return,finally中无return,仅仅修改基本数据类型
当是如下场景的时候
package com.finallydemo;
/**
* @author wty
* @date 2024/10/7 21:25
*/
public class FinallyDemo {
public static void main(String[] args) throws Exception {
int i = testFinally();
System.out.println(i);
}
public static int testFinally() throws Exception {
int i = 1;
try {
i = 2;
return i;
} finally {
i = 3;
}
}
}
输出结果:
2
结论:finally 中修改基本类型不会影响 try 、catch 中 return 中的返回值
场景二、try代码块中有return,finally中无return,仅仅修改引用类型
类Person如下
class Person{
Integer age;
public Person(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}
}
代码如下:
public static Person testFinallyPersion() throws Exception {
Person person = new Person(3);
try {
person.setAge(4);
return person;
} finally {
person.setAge(5);
}
}
输出的结果是:
Person{age=5}
结论:当为返回值为引用类型时,返回的其实是一个地址,我们使用地址修改了原内容
场景三、try代码块中有return,finally中无return,新建引用类型并修改
代码如下:
public static Person testFinallyPersion() throws Exception {
Person person = new Person(3);
try {
person.setAge(4);
return person;
} finally {
person = new Person(5);
}
}
输出结果:
Person{age=4}
我们其实将 person 指向了新的地址new Person(),因此并没有修改原返回值地址的内容。
场景四、try代码块中有return,finally中也有return,仅仅修改基本数据类型
代码如下:
public static int testFinally() throws Exception {
int i = 1;
try {
i = 2;
return i;
} finally {
i = 3;
return i;
}
}
输出
3
当我们在 finally 中使用 return 时,try 或 catch 中的 return 会失效或异常丢失,会在 finally 直接返回。
场景五、try代码块中有return,finally中也有return,仅仅修改引用类型
代码如下:
public static Person testFinallyPersion() throws Exception {
Person person = new Person(3);
try {
person.setAge(4);
return person;
} finally {
person.setAge(5);
return person;
}
}
输出:
Person{age=5}
我们其实将 person 指向了新的地址new Person(),因此并没有修改原返回值地址的内容。
场景六、try代码块中有return,finally中也有return,新建引用类型并修改
代码如下:
public static Person testFinallyPersion() throws Exception {
Person person = new Person(3);
try {
person.setAge(4);
return person;
} finally {
person = new Person(5);
return person;
}
}
输出:
Person{age=5}
我们其实将 person 指向了新的地址new Person(),因此并没有修改原返回值地址的内容。
场景七、try代码块中有异常和return,finally中也有return,仅仅修改基本数据类型 ☆
代码如下:
public static int testFinally() throws Exception {
int i = 1;
try {
i = 2;
if (true) {
System.out.println(1 / 0);
}
return i;
} finally {
i = 3;
return i;
}
}
输出:
3
当我们在 finally 中使用 return 时,try 或 catch 中的 return 会失效或异常丢失,会在 finally 直接返回。
这就是我异常失效的原因。
场景八、try代码块中有异常和return,finally中也有return,仅仅修改引用类型
public static Person testFinallyPersion() throws Exception {
Person person = new Person(3);
try {
person.setAge(4);
if (true) {
System.out.println(1 / 0);
}
return person;
} finally {
person.setAge(5);
return person;
}
}
输出:
Person{age=5}
我们其实将 person 指向了新的地址new Person(),因此并没有修改原返回值地址的内容。
场景九、try代码块中有异常和return,finally中也有return,新建引用类型并修改
代码如下:
public static Person testFinallyPersion() throws Exception {
Person person = new Person(3);
try {
person.setAge(4);
if (true) {
System.out.println(1 / 0);
}
return person;
} finally {
person = new Person(5);
return person;
}
}
输出:
Person{age=5}
我们其实将 person 指向了新的地址new Person(),因此并没有修改原返回值地址的内容。
场景十、finally代码一定会执行吗
答案:不是的
请看代码
public static Person testFinallyPersion() throws Exception {
Person person = new Person(3);
try {
person.setAge(4);
System.exit(0);
return person;
} finally {
person.setAge(5);
return person;
}
}
输出:这里什么也没输出
原因很简单,System.exit(0);已经结束了,不会再跑finally中的代码块了。
五、finally总结
以下总结转自CSDN的另一个博主,我觉得总结的很好
finally 底层原理分析
- 《The JavaTM Virtual Machine Specification, Second Edition》 一书中我们可以知道 Java 虚拟机是如何编译 finally:实际上,Java 虚拟机会把 finally 语句块作为 subroutine(子语句)
直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。- 还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值(基本类型值或地址)到**本地变量表(Local Variable Table)**中,待 subroutine
执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。- 理解了 JVM 对 finally 的实现,我们其实就很好理解 finally 中修改数据的影响 中的案例,有兴趣的朋友可以下去深入了解。
原文链接:原文链接