技术知识点
@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」》