黑马程序员JavaWeb开发教程P174员工表记录删除员工操作表记录正常写入解决方案

一、背景

原视频参考该链接
黑马程序员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 中修改数据的影响 中的案例,有兴趣的朋友可以下去深入了解。

原文链接:原文链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心向阳光的天域

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值