Java 程序员成长记(三):菜鸟入职之@Transactional「罢工」

技术知识点

@Transactional作用域、Spring AOP代理机制、自调用问题解决方案、事务隔离级别、手动回滚方法

一、红色警告的「事务迷局」

周一清晨,暴雨敲打着XX科技的落地窗。小彬盯着控制台的红色异常,手心比窗外的空气还潮湿。他刚写完的任务创建功能,在抛出IllegalArgumentException后,数据库居然没回滚------10条测试数据像顽固的污渍,死死钉在表里。

"不可能啊,我明明加了@Transactional。"他喃喃自语,疯狂翻动代码。注解确实加在Service方法上,异常也继承自RuntimeException,按文档说的应该自动回滚才对。工位隔板突然被敲响,大飞哥探进头来,POLO衫领口别着枚"流程控"徽章,甘特图上的任务条被雨水洇得模糊。

"怎么,被事务管理摆了一道?"大飞哥扫了眼代码,突然笑出声,"新人嘛,总要尝尝Spring代理的'玄学'。"他拖过椅子,指尖在屏幕上画圈:"你这方法是private的?"

小彬脸一热:"之前写传统企业代码习惯了,觉得内部调用用private更安全..."

"安全个锤子!"大飞哥敲了敲显示器,"Spring AOP只代理public方法,这是铁律。就像甘特图里的任务,不标成'公开可见',其他人根本没法协同。"他抄起鼠标把方法改成public,"再试一次。"

二、大飞哥的「代理玄学」

重启项目,小彬屏住呼吸点击测试按钮。异常如期抛出,可数据库依然纹丝不动。大飞哥皱起眉头,从抽屉里摸出一本《Spring Boot核心技术解析》,哗啦翻到AOP章节:"你是不是在类内部调用了这个方法?"

小彬回想代码:"对啊,我在同一个Service里,先调用createTask(),然后调用sendNotification(),结果异常出现在createTask里..."

"这就是关键!"大飞哥拍了下桌子,震得保温杯里的枸杞晃悠,"Spring的事务是基于代理实现的,你在类内部直接调方法,用的是'真身',不是代理对象,当然不会触发事务!"他在白板上画起代理模式示意图:"就像明星出席活动,你得通过经纪人(代理)才能走流程,直接找本人没用。"

小彬恍然大悟:"那该怎么调用?"大飞哥嘴角扬起神秘的微笑,在代码里加上一行:

java 复制代码
import org.springframework.aop.framework.AopContext;

public class TaskService {
    public void createTask(Task task) {
        // 错误调用:this.sendNotification()
        AopContext.currentProxy().sendNotification(task.getId());
    }

    @Transactional
    public void sendNotification(Long taskId) {
        // 发送通知逻辑,抛异常
        throw new IllegalArgumentException("通知失败");
    }
}

"用AopContext.currentProxy()获取代理对象,相当于让经纪人去干活。"大飞哥解释道,"不过记得在application.properties里加一句spring.aop.proxy-target-class=true,启用CGLIB代理,默认是JDK动态代理,可能有坑。"

再次测试时,异常抛出的瞬间,数据库记录应声消失。小彬长舒一口气,发现大飞哥的手机突然响起熟悉的旋律------居然是《童话》的调子,但歌词变成了:"你哭着对我说,代理模式都是真的..."

三、茶水间的「AOP哲学」

午休时,小彬在茶水间遇到老王。架构师正对着咖啡机研究"分布式事务解决方案"手册,闻言笑道:"听说你被代理模式上了一课?这玩意儿确实像玄学,当年我带的实习生把@Transactional加在接口实现类的private方法上,debug了三天没找到原因,最后在代码评审会上被我当众处刑。"

"为什么Spring不代理非public方法呢?"小彬递上咖啡。老王接过杯子,在吧台画了个UML类图:"从设计原则讲,非public方法属于类的内部实现,不该被外部代理干预。就像你写代码时,private方法本就是供内部调用的,加事务反而破坏封装性。"

这时,贝贝哥抱着马克杯路过,杯身上"代码即文档"字样映着窗外的雨光:"其实还有更优雅的解法。"他擦了擦眼镜,"如果非要在类内调用带事务的方法,可以拆分成两个Service,通过依赖注入互相调用,避免自引用。"他掏出手机展示自己的代码:

java 复制代码
@Service
public class TaskService {
    private final NotificationService notificationService;

    public TaskService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void createTask(Task task) {
        // 正确调用:通过注入的Service调用
        notificationService.sendNotification(task.getId());
    }
}

@Service
public class NotificationService {
    @Transactional
    public void sendNotification(Long taskId) {
        // 业务逻辑
    }
}

"代码洁癖第三条:永远用依赖注入代替自我引用。"贝贝哥晃了晃杯子,"不过大飞哥的代理解法更直接,适合紧急排坑------各有优劣吧。"

四、暴雨夜的「代理突击」

下班后,暴雨升级成雷暴。小彬留在办公室整理事务管理笔记,突然想起老王说的"代理模式是Spring的地基",于是打开Spring官网文档,仔细研读AOP实现原理。当他终于搞懂JDK动态代理和CGLIB的区别时,抬头看见大飞哥居然还在工位上改甘特图。

"怎么还不走?"大飞哥披上外套,POLO衫换成了印有"流程控永不加班"的卫衣,"带你去见识下真正的代理玄学。"他领着小彬来到测试环境,打开监控工具:"看这个接口,表面上用了@Transactional,实际因为异常被try-catch吞了,事务根本没回滚。"

小彬凑近屏幕,只见日志里静静地躺着try{...}catch(Exception e){...}代码块。大飞哥摇摇头:"这是新人最容易犯的错,以为加了注解就万事大吉,却不知道手动捕获异常会让事务失效。记住,@Transactional的正确用法是:要么抛出去,要么在catch里加TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。"

回到开发机前,小彬在自己的代码里添加了异常处理逻辑:

java 复制代码
@Transactional
public void sendNotification(Long taskId) {
    try {
        // 业务逻辑
    } catch (Exception e) {
        // 手动设置回滚
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        throw new RuntimeException("通知失败", e);
    }
}

"这就对了。"大飞哥赞许地点头,"事务管理就像项目管理,表面看是流程问题,实际处处都是细节陷阱。就像甘特图里的里程碑,每个节点都得卡死,不然整个项目就会失控。"

五、第二天的「红姐拷问」

次日清晨,红姐带着新的测试用例杀到工位:"听说你的事务回滚修好了?来测测并发场景。"她打开压测工具,同时发起100个任务创建请求,每个请求都故意包含重复数据。

小彬紧张地盯着数据库监控。前50个请求顺利回滚,第51个却突然成功提交------这意味着事务在并发下失效了。红姐的笔尖在测试报告上敲出哒哒声:"说说,为什么会出现这种情况?"

大飞哥不知何时站在身后:"因为默认的事务隔离级别是READ_COMMITTED,并发插入重复数据时可能出现脏读。"他转向小彬,"记得给方法加上@Transactional(isolation = Isolation.REPEATABLE_READ),提高隔离级别。"

修改注解后再次压测,所有重复请求都正确回滚了。红姐在测试报告上画了个勾:"勉强通过。但记住,事务隔离级别越高,性能损耗越大,就像项目范围越广,延期风险越高------这就是大飞哥挂在墙上的'项目铁三角'原理。"

六、技术梗的「终极形态」

午休时,小彬的电脑突然弹出一封邮件,发件人是大飞哥,主题为"代理模式之歌.mp3"。点开后,熟悉的旋律响起:"我愿变成,你的代理对象,守护着你回滚,直到永久~"歌词改编得让人忍俊不禁,附件里还有张"代理大师"电子勋章,图案是一个戴着礼帽的经纪人形象。

贝贝哥路过时瞥见屏幕,递来一张新的键盘贴纸:"刚从老王那里顺的,适合你现在的心境。"贴纸上面写着:"@Transactional不是魔法咒语,是代理对象的契约"。小彬摸着贴纸,突然想起昨晚大飞哥说的话:"每个技术点都该有温度,不然写代码和搬砖有什么区别?"

下班前,小彬在笔记本上写下:"事务管理的本质是控制'一致性',就像团队协作要控制'进度一致性'。代理模式不是玄学,是Spring用设计模式织就的保护网。而我,终于学会了在这张网里跳舞。"

窗外的暴雨已经停了,夕阳穿过云层,在大飞哥办公室的"项目铁三角"标语上洒下金边。小彬看着自己工位上新增的"代理模式之歌"鼠标垫,突然觉得这个充满代码和谜题的世界,开始变得亲切而有趣。

【下章预告】

《Java 程序员成长记(四):菜鸟入职之接口「前后端 Battle」》

相关推荐
怀旧,5 分钟前
【数据结构】6. 时间与空间复杂度
java·数据结构·算法
大春儿的试验田1 小时前
Parameter ‘XXX‘ not found. Available parameters are [list, param1]
java
程序员JerrySUN1 小时前
[特殊字符] 深入理解 Linux 内核进程管理:架构、核心函数与调度机制
java·linux·架构
2302_809798321 小时前
【JavaWeb】Docker项目部署
java·运维·后端·青少年编程·docker·容器
zhojiew2 小时前
关于akka官方quickstart示例程序(scala)的记录
后端·scala
网安INF2 小时前
CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
java·web安全·网络安全·flink·漏洞
一叶知秋哈2 小时前
Java应用Flink CDC监听MySQL数据变动内容输出到控制台
java·mysql·flink
jackson凌2 小时前
【Java学习笔记】SringBuffer类(重点)
java·笔记·学习
sclibingqing2 小时前
SpringBoot项目接口集中测试方法及实现
java·spring boot·后端