教会你

Posted iecne

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了教会你相关的知识,希望对你有一定的参考价值。



🧛‍♂️iecne个人主页:iecne的学习日志
💡每天关注iecne的作品,一起进步
💪学C++必看iecne
本文专栏【C++游戏引擎】.
🐳希望大家多多支持🥰一起进步呀!


✨前言

互相学习,或者加入粉丝群,可以添加vx YAN_Pisces_Boom
哈喽大家好,我是iecne,本期为大家带来的是CPP/C++【游戏引擎Easy2D】绘制一个简单的二维图形,例如矩形、圆形等。包教包会,快来看看吧!
引擎支持 Visual Studio 2013 及以上版本,如果你使用的是较低版本的 VS,那么你需要考虑一下更新你的编译器了

任务描述

如何根据据Button制作鼠标控制按钮类
本实践旨在通过多个代码的案列,让大家理解和掌握如何使用Easy2D引擎来绘制一个简单的二维图形,例如矩形、圆形等。


文章目录


Easy2D导读

对于编程而言,初学者最需要的不是技能,而是成就感。
如果你喜欢用 C/C++ 编写自己的小游戏,那么 Easy2D 将是个不错的选择,它大大简化了游戏制作过程,可以帮助你快速开发 Windows 上的 2D 小游戏。
它的特点和它的名字一样,Everything is Easy!


安装 Easy2D

使用简易安装器
运行里面的 install.bat 文件进行安装。
点击此链接进行下载(纯免费,给官方搞成了VIP)
点击下载
安装程序会检测您已经安装的 Visual Studio 版本,并根据您的选择将对应库文件解压至 VS 库目录下。
Easy2D 不支持 VS2012 及以下的版本(包括 VC6.0),如果你使用的是低版本的 VS,那你需要尝试一下新版的 Visual Studio 了。

安装环境:
操作系统:Windows 7 及以上操作系统。
编译环境:Visual Studio 2013 (x86 & x64) 及以上版本。


一.Easy2D图形

Shape 是节点的一种,它用于绘制一个简单的二维图形,例如矩形、圆形等。

1.1 形状分类

Easy2D 仅提供了四种简单图形,分别是

  • RectShape 矩形
  • RoundRectShape 圆角矩形
  • CircleShape 圆形
  • EllipseShape 椭圆形

1.2 形状的使用方法

创建四种形状的方式分别为:

// 创建一个宽高为 (10,20) 的矩形
auto rect = gcnew RectShape(Size(10, 20));
// 创建一个宽高为 (10,20) ,圆角角度为 (40,20) 的圆角矩形
auto roundRect = gcnew RoundRectShape(Size(10, 20), 40, 20);
// 创建一个半径为 10 的圆形
auto circle = gcnew CircleShape(10);
// 创建一个半径为 (10,20) 的椭圆形
auto ellipse = gcnew EllipseShape(10, 20);

因为形状也是节点的一种,所以具备节点的所有性质,例如移动

// 创建一个宽高为 (10,20) 的矩形
auto rect = gcnew RectShape(Size(10, 20));
// 移动矩形到 (100, 100) 坐标处
rect->setPos(100, 100);

在这里我们的移动是

rect->setPos(100, 100);

(100,100)则是坐标

若我们需要移动到100,200,则代码如下

rect->setPos(100, 200);

前提是,你的窗口大小一定要大于100,200,这样才能出现在窗口中


1.3 填充与轮廓

接下来我们详细讲解如何使用填充和轮廓

1.3.1 理论

所有的形状都可以设置样式,样式包括以下三种:

  • 填充
  • 轮廓
  • 填充+轮廓
enum Style

    Solid,  /* 填充 */
    Round,  /* 轮廓 */
    Fill,   /* 轮廓 + 填充 */
;

Tip

  • 轮廓: 解释为构成图形或物体的外缘的线条
  • 填充: 解释为图形没有轮廓,中间实心
  • 轮廓+填充:结合1、2

1.3.2 实例使用

例如,设置一个形状仅显示轮廓,不显示填充颜色:

// 设置形状样式为,轮廓
shape->setStyle(Shape::Style::Round);

所有的形状都可以设置填充色和轮廓颜色

创建一个宽高为 (10,20) 的矩形

auto rect = gcnew RectShape(Size(10, 20));

设置填充颜色为红色

rect->setFillColor(Color::Red);

设置轮廓颜色为白色

rect->setLineColor(Color::White);

设置轮廓线条宽度为 2

rect->setStrokeWidth(2.0);

合起来如下:

// 创建一个宽高为 (10,20) 的矩形
auto rect = gcnew RectShape(Size(10, 20));
// 设置填充颜色为红色
rect->setFillColor(Color::Red);
// 设置轮廓颜色为白色
rect->setLineColor(Color::White);
// 设置轮廓线条宽度为 2
rect->setStrokeWidth(2.0);

二.对比EasyX图形库

C++/C还有一个可视化的非引擎,名为EasyX
他们都有Easy,让作者联想起他们的关联
由于作者两个都会,所以就拿过来对比一下

setbkcolor 设置当前设备绘图背景色。
setbkmode 设置当前设备图案填充和文字输出时的背景模式。
setfillcolor 设置当前设备填充颜色。
setfillstyle 设置当前设备填充样式。
setlinecolor 设置当前设备画线颜色。
setlinestyle 设置当前设备画线样式。


三.关于引擎的杂谈

两者区别不大,但是EasyX的更加全面,毕竟EasyX 是针对 C/C++ 的图形库的。所以EasyX的画图作用高于Easy2D
但是我们在编写游戏的时候,可以EasyX和Easy2D结合。博主以前写过一个程序,由于Easy2D的MessegeBox没有EasyX的功能,我就直接加入EasyX的库,将EasyX和Easy2D结合,这样就更好了
还有,如果我们在写项目的时候,也可以加入这两个库,对于我们Cpp写程序的时候有益无害
但是写题目的时候千万不要加入这两个库,因为我们很多的测试平台不会加上Easy2D和EasyX,这就和你写题目用Unity3D、虚幻等大引擎,编译错误一样的,区别不大
对于初学者,没有专门学游戏开发的Cpp学者,还是建议学一下Easy2D,这样可以拓展知识面,而Unity2D和虚幻等国际有名的大引擎,对于我们来说难度偏高,学入门所需要的时间不是一时半会儿的,所以我们用Easy2D拓展知识面熟悉一下引擎的样子,其实Easy2D和QT的区别也不会特别大,一通百通,学会举一反三即可。


关于命名

Easy2D 按照以下规律命名函数:

小驼峰式命名法则,第一个单词小写,后面的单词首字母大写,如Window::setTitle
所有函数均按照动词+名语形式命名,如Logger::showConsole
获取对象的属性值:get+属性名,如Node::getWidth
修改对象的属性值:set+属性名,如Node::setPos
获取对象的状态(bool值):is+状态名,如Node::isVisiable


总结

本节课主要是讲解了Easy2D引擎实现的教程,至此该课示例的思想博主已经真真切切彻彻底底分享完了,相信大家对这个逻辑有了一定的理解,大家可以自己动手敲敲代码,感受一下,包教包会。若有任何侵权和不妥之处,请私信于我,定积极配合处理,看到必回!!!

帅的人已然点赞收藏关注,而丑的人还在犹豫,被犹豫了,快三连吧!
原创不易,还希望各位大佬支持一下
点赞,你的认可是我创作的动力!
收藏,你的青睐是我努力的方向!
评论,你的意见是我进步的财富!


感谢每一个观看本篇文章的朋友,更多精彩敬请期待~( ̄▽ ̄~)~:
iecne的博客
若有任何侵权和不妥之处,请私信于我,定积极配合处理,看到必回!!!

,教会你一种阅读源码的方式。

你好呀,我是歪歪。

是的,正如标题描述的这样,我试图通过这篇文章,教会你如何阅读源码。

事情大概是这样的,前段时间,我收到了一个读者发来的类似于这样的示例代码:

他说他知道这三个案例的回滚情况是这样的:

  • insertTestNoRollbackFor:不会回滚
  • insertTestRollback:会回滚
  • insertTest:会回滚

他说在没有执行代码之前,他也知道前两个为什么一个不会回滚,一个会回滚。因为抛出的异常和 @Transactional 里面的注解呼应上了。

但是第三个到底会不会回滚,没有执行之前,他不知道为什么会回滚。执行之后,回滚了,他也不知道为什么回滚了。

我告诉他:源码之下无秘密。

让他去看看这部分源码,理解它的原理,不然这个地方抛出一个其他的异常,又不知道会不会回滚了。

但是他说他完全不会看源码,找不到下手的角度。

所以,就这个问题,我打算写这样的一篇文章,试图教会你一种阅读源码的方式。让你找到一个好的切入点,或者说突破口。

但是需要事先说明的是,阅读源码的方式非常的多,这篇文章只是站在我个人的角度介绍阅读源码的众多方式中的一种,沧海一粟,就像是一片树林里面的一棵树的树干上的一叶叶片的叶脉中的一个小分叉而已。

对于啃源码这件事儿,没有一个所谓的“一招吃遍天下”的秘诀,如果你非要让我给出一个秘诀的话,那么就只有一句话:

啃源码的过程,一定是非常枯燥的,特别是啃自己接触不多的框架源码的时候,千头万绪,也得下手去捋,所以一定要耐得住寂寞才行。

然后,如果你非得让我再补充一句的话,那么就是:

调试源码,一定要亲!自!动!手!只是去看相关的文章,而没有自己一步步的去调试源码,那你相当于看了个寂寞。

亲自动手的第一步就是搞个 Demo 出来。用“黑话”来说,这个 Demo 就是你的抓手,有了抓手你才能打出一套理论结合实际的组合拳。抓手多了,就能沉淀出可复用的方法论,最终为自己赋能。

搭建 Demo

所以,第一步肯定是先把 Demo 给搭建起来,项目结构非常的简单,标准的三层结构:

主要是一个 Controller,一个 Service,然后搞个本地数据库给接上,就完全够够的了:

Student 对象是从表里面映射过来的,随便弄了两个字段,主要是演示用:

就这么一点代码,给你十分钟,你是不是就能搭建好了?中间甚至还能摸几分钟鱼。

要是只有这么一点东西的、极其简单的 Demo 你都不想自己亲自动手搭一下,然后自己去调试的话,仅仅是通过阅读文章来肉眼调试,那么我只能说:

在正式开始调试代码之前,我们还得明确一下调试的目的:想要知道 Spring 的 @Transactional 注解对于异常是否应该回滚的判断逻辑具体是怎么样的。

带着问题去调试源码,是最容易有收获的,而且你的问题越具体,收获越快。你的问题越笼统,就越容易在源码里面迷失。

方法论之关注调用栈

自己 Debug 的过程就是不断的打断点的过程。

我再说一次:自己 Debug 的过程就是不断的打断点的过程。

打断点大家都会打,断点打在哪些地方,这个玩意就很讲究了。

在我们的这个 Demo 下,第一个断点的位置非常好判断,就打在事务方法的入口处:

一般来说,大家调试业务代码的时候,都是顺着断点往下调试。但是当你去阅读框架代码的时候,你得往回看。

什么是“往回看”呢?

当你的程序在断点处停下的时候,你会发现 IDEA 里面有这样的一个部分:

这个调用栈是你在调试的过程中,一个非常非常非常重要的部分。

它表示的是以当前断点位置为终点的程序调用链路。

为了让你彻底的明白这句话,我给你看一张图:

我在 test6 方法中打上断点,调用栈里面就是以 test6 方法为终点到 main 方法为起点的程序调用链接。

当你去点击这个调用栈的时候,你会发现程序也会跟着动:

“跟着动”的这个动作,你可以理解为你站着断点处“往回看”的过程。

当你理解了调用栈是干啥的了之后,我们再具体看看在当前的 Demo 下,这个调用栈里面都有写啥:

标号为 ① 的地方,是 TestController 方法,也就是程序的入口。

标号为 ② 的地方,从包名称可以看出是 String AOP 相关的方法。

标号为 ③ 的地方,就可以看到是事务相关的逻辑了。

标号为 ④ 的地方,是当前断点处。

好,到这里,我想让你简单的回顾一下你来调试代码的目的是什么?

是不是想要知道 Spring 的 @Transactional 注解对于异常是否应该回滚的判断逻辑具体是怎么样的。

那么,我们是不是应该主要把关注的重点放在标号为 ③ 的地方?

也就是对应到这一行:

这个地方我一定要特别的强调一下:要保持目标清晰,很多人在源码里面迷失的原因就是不知不觉间被源码牵着走远了。

比如,有人看到标号为 ② 的部分,也就是 AOP 的部分,一想着这玩意我眼熟啊,书上写过 Spring 的事务是基于 AOP 实现的,我去看看这部分代码吧。

当你走到 AOP 里面去的时候,路就开始有点走偏了。你明白我意思吧?

即使在这个过程中,你翻阅了这部分的源码,确实了解到了更多的关于 AOP 和事务之间的关系,但是这个部分并不解决你“关于回滚的判断”这个问题。

然而更多更真实的情况可能是这样的,当你点到 AOP 这部分的时候,你一看这个类名称是 CglibAopProxy:

你一细嗦,Cglib 你也熟悉啊,它和 JDK 动态代理是一对好兄弟,都是老八股了。

然后你可能又会点击到 AopProxy 这个接口,找到 JdkDynamicAopProxy:

接着你恍然大悟:哦,我在什么都没有配置的情况下,当前版本的 SpringBoot 默认使用的是 Cglib 作为动态代理的实现啊。

诶,我怎么记得我背的八股文默认是使用 JDK 呢?

网上查一下,查一下。

哦,原来是这么一回事儿啊:

  • SpringBoot 1.x,默认使用的是 JDK 动态代理。
  • SpringBoot 2.x 开始,为了解决使用 JDK 动态代理可能导致的类型转化异常而默认使用 CGLIB。
  • 在 SpringBoot 2.x 中,如果需要默认使用 JDK 动态代理可以通过配置项spring.aop.proxy-target-class=false来进行修改,proxyTargetClass配置已无效。

刚刚提到了一个 spring.aop.proxy-target-class 配置,这是个啥,咋配置啊?

查一下,查一下...

喂,醒一醒啊,朋友,走远了啊。还记得你调试源码的目的吗?

如果你对于 AOP 这个部分感兴趣,可以先进行简单的记录,但是不要去深入的追踪。

不要觉得自己只是随便看看,不要紧。反正正是因为这些“随便看看”导致你在源码里面忙了半天感觉这波学到了,但是停下来一想:我 TM 刚刚看了些啥来着?我的问题怎么还没解决?

我为什么要把这部分非常详尽,甚至于接近啰嗦的写一遍,就是因为这个就是初看源码的朋友最容易犯的错误。

特别强调一下:抓住主要矛盾,解决主要问题。

好,回到我们通过调用栈找到的这个和事务相关的方法中:

org.springframework.transaction.interceptor.TransactionInterceptor#invoke

这个方法,就是我们要打第二个断点,或者说这才是真正的第一个断点的地方。

然后,重启项目,重新发起请求,从这个地方就可以进行正向的调试,也就是从框架代码一步步的往业务代码执行。

比如这个方法接着往下 Debug,就来到了这个地方:

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

找到了这个地方,你就算是无限的接近于问题的真相了。

这个部分我肯定会讲的,但是在这里先按下不表,毕竟这并不是本文最重要的东西。

本文最重要的是,我再次重申一遍:我试图想要教会你一种阅读源码的方式,让你找到一个好的切入点,或者说突破口。

由于这个案例比较简单,所以很容易找到真正的第一个利于调试的断点。

如果遇到一些复杂的场景、响应式的编程、异步的调用等等,可能会循环往复的执行上面的动作。

分析调用栈,打断点,重启。

再分析调用栈,再打断点,再重启。

方法论之死盯日志

其实我发现很少有人会去注意框架打印的日志,就像是很少有人会去仔细阅读源码上的 Javadoc 一样。

但是其实通过观察日志输出,也是一个很好的寻找阅读源码突破口的方式。

我们要做的,就是保证 Demo 尽量的单纯,不要有太多的和本次排查无关的代码和依赖引入。

然后把日志级别修改为 debug:

logging.level.root=debug

接着,就是发起一次调用,然后耐着性子去看日志。

还是我们的这个 Demo,发起一次调用之后,控制台输出了很多的日志,我给你搞个缩略图看看:

我们已知的是这里面大概率是有线索的,有没有什么方法尽量快的找出来呢?

有,但是通用性不强。所以如果经验不够丰富的话,那么最好的方法就是一行行的去找。

前面我也说过了:啃源码的过程,一定是非常枯燥的。

所以你一定会找到这样的日志输出:

Acquired Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] to manual commit
...
==>  Preparing: insert into student ( name, home ) values ( ?, ? ) 
HikariPool-1 - Pool stats (total=1, active=1, idle=0, waiting=0)
==> Parameters: why(String), 草市街199号-insertTestNoRollbackFor(String)
<==    Updates: 1
...
Committing JDBC transaction on Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c]
Releasing JDBC Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] after transaction

这几行日志,不就是正对应着 Spring 事务的开启和提交吗?

有了日志,我们完全可以基于日志去找对应的日志输出的地方,比如我们现在要找这一行日志输出对应的代码:

o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] for JDBC transaction

首先,我们可以根据日志知道对应输出的类是 DataSourceTransactionManager 这个类。

然后找到这个类,按照关键词搜索:

不就找到这一行代码了吗?

或者我们直接秉承大力出奇迹的真理,来一个暴力的全局搜索,也是能搜到这一行代码的:

再或者修改一下日志输出格式,把行号也搞出来嘛。

当我们把日志格式修改为这样之后:

logging.pattern.console=%ddd-MM-yyyy HH:mm:ss.SSS %magenta([%thread]) %highlight(%-5level) %logger.%M:%L - %msg%n

控制台的日志就变成了这样:

org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin:263 - Acquired Connection [HikariProxyConnection@1569067488 wrapping com.mysql.cj.jdbc.ConnectionImpl@19a49539] for JDBC transaction

很直观的就看出来了,这行日志是 DataSourceTransactionManager 类的 doBegin 方法,在 263 行输出的。

然后你找过去,发现没有任何毛病,这就是案发现场:

我前面给你说这么多,就是为了让你找到这一行日志输出的地方。

现在,找到了,然后呢?

然后肯定就是在这里打断点,然后重启程序,重新发起调用了啊。

这样,你又能得到一个调用栈:

然后,你会从调用栈中看到一个我们熟悉的东西:

朋友,这不就和前面写的“方法论之关注调用栈”呼应起来了吗?

这不就是一套组合拳吗,不就是沉淀出的可复用的方法论吗?

黑话,咱们也是可以整两句的。

方法论之查看被调用的地方

除了前面两种方法之外,我有时候也会直接看我要阅读部分的方法,在框架中被哪些地方调用了。

比如在我们的 Demo 中,我们要阅读的代码非常的明确,就是 @Transactional 注解。

于是直接看一下这个注解在哪些地方用到了:

有的时候调用的地方会非常的少,甚至只有一两处,那么直接在调用的地方打上断点就对了。

虽然 @Transactional 注解一眼望去也是有很多的调用,但是仔细一看大多是测试类。排除测试类、JavaDoc 里面的备注和自己项目中的使用之后,只剩下很明显的这三处:

看起来很接近真相,但是很遗憾,这里只是在项目启动的时候解析注解而已。和我们要调研的地方,差的还有点远。

这个时候就需要一点经验了,一看苗头不对,立马转换思路。

什么是苗头不对呢?

你在这几个地方打上断点了,只是在项目启动的过程中断点起作用了,发起调用的时候并没有在断点处停下,说明发起调用的时候并不会触发这部分逻辑,苗头不对。

顺着这个思路想,在我的 Demo 中抛出了异常,那么 rollbackFor 和 noRollbackFor 这两个参数大概率是会在调用的时候被用到,对吧?

所以当你去看 rollbackFor 被调用的时候只有我们自己写的业务代码在调用:

怎么办呢?

这个时候就要靠一点运气了。

是的,靠运气。

你都点到 rollbackFor 这个方法来了,你也看了它被调用的地方,在这个过程中你大概率会瞟到几眼它对应的 JavaDoc:

org.springframework.transaction.annotation.Transactional#rollbackFor

然后你会发现在 JavaDoc 里面提到了 rollbackOn 这个方法:

org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable)

到这里一看,你发现这是一个接口,它有好多个实现类:

怎么办呢?

早期的时候,由于不知道具体的实现类是哪个,我是在每个实现类的入口处都打上断点,虽然是笨办法,但是总是能起作用的。

后来我才发现,原来可以直接在接口上打断点:

然后,重启项目,发起调用,第一次会停在我们方法的入口:

F9,跳过当前断点之后,来到了这个地方:

这里就是我前面在接口上打的方法断点,走到了这个实现类中:

org.springframework.transaction.interceptor.DelegatingTransactionAttribute

然后,关键的就来了,我们又有一个调用栈了,又从调用栈中看到一个我们熟悉的东西:

朋友,组合拳这不又打起来了?突破口不就又找到了?

关于“瞟到几眼对应的 JavaDoc ,然后就可能找到突破口”的这个现象,早期对我来说确实是运气,但是现在已经是一个习惯了。一些知名框架的 JavaDoc 真的写的很清楚的,里面隐藏了很多关键信息,而且是最权威的正确信息,读官网文档,比读技术博客稳当的多。

探索答案

前面我介绍的都是找到代码调试突破口的方法。

现在突破口也有了,接下来应该怎么办呢?

很简单,调试,反复的调试。从这个方法开始,一步一步的调试:

org.springframework.transaction.interceptor.TransactionInterceptor#invoke

如果你真的想要有所收获的话,这是一个需要你亲自去动手的步骤,必须要有逐行阅读的一个过程,然后才能知道大概的处理流程。

我就不进行详细解读了,只是把重点给大家画一下:

框起来的部分,就是去执行业务逻辑,然后基于业务逻辑的处理结果,去走不同的逻辑。

抛异常了,走这个方法:completeTransactionAfterThrowing

正常执行完毕了,走这个方法:commitTransactionAfterReturning

所以,我们问题的答案就藏在 completeTransactionAfterThrowing 里面。

继续调试,进入这个方法之后,可以看到它拿到了事务和当前异常相关的信息:

在这个方法里面,大体的逻辑是当标号为 ① 的地方为 true 的时候,就在标号为 ② 的地方回滚事务,否则就在标号为 ③ 的地方提交事务:

因此,标号为 ① 的部分就很重要了,这里面就藏着我们问题的答案。

另外,在这里多说一句,在我们的案例中,这个方法,也就是当前调试的方法是不会回滚的:

而这个方法是会回滚的:

也就是这两个方法在这个地方会走不同的逻辑,所以你在调试的时候遇到 if-else 就需要注意,去构建不同的案例,以覆盖尽量多的代码逻辑。

继续往下调试,会进入到标号为 ① 的 rollbackOn 方法里面,来到这个方法:

org.springframework.transaction.interceptor.RuleBasedTransactionAttribute#rollbackOn

这里,就藏着问题的终极答案,而且这里面的代码逻辑相对比较的绕。

核心逻辑就是通过循环 rollbackRules,这里面装的是我们在代码中配置的回滚规则,在循环体中拿 ex,也就是我们程序抛出的异常,去匹配规则,最后选择一个 winner:

如果 winner 为空,则走默认逻辑。如果是 RuntimeException 或者是 Error 的子类,就要进行回滚:

如果有 winner,判断 winner 是否是不用回滚的配置,如果是,则取反,返回 false,表示不进行回滚:

那么问题的冠军就在于:winner 怎么来的?

答案就藏着这个递归调用中:

一句话描述就是:看当前抛出的异常和配置的规则中的 rollbackFor 和 noRollbackFor 谁距离更近。这里的距离指的是父类和子类之间的关系。

比如,还是这个案例:

我们抛出的是 RuntimeException,它距离 noRollbackFor=RuntimeException.class 为 0。RuntimeException 是 Exception 的子类,所以距离 rollbackFor = Exception.class 为 1。

所以,winner 是 noRollbackFor,能明白吧?

然后,我们再看一下这个案例:

根据前面的“距离”的分析,NullPointerException 是 RuntimeException 的子类,它们之间的距离是 1。而 NullPointerException 到 Exception 的距离是 2:

所以,rollbackFor=RuntimeException.class 这个的距离更短,所以 winner 是 rollbackFor。

而把 winner 放到这个判断中,返回是 true:

return !(winner instanceof NoRollbackRuleAttribute);

所以,这就是它为什么会回滚的原因:

好了,到这里你有可能是晕的,晕就对了,去调试这部分代码,亲自摸一遍,你就搞的明明白白了。

最后,再给“死盯日志”的方法论打个补丁吧。

前面我说了,日志级别调整到 Debug 也许会有意外发现。现在,我要再给你说一句,如果 Debug 没有查到信息,可以试着调整到 trace:

logging.level.root=trace

比如,当我们调整到 trace 之后,就可以看到“ winner 到底是谁”这样的信息了:

当然了,trace 级别下日志更多了。

所以,来,再跟我大声的读一遍:

啃源码的过程,一定是非常枯燥的,特别是啃自己接触不多的框架源码的时候,千头万绪,也得下手去捋,所以一定要耐得住寂寞才行。

作业

我前面主要是试图教你一种阅读源码时,寻找突破点的技能。这个突破点,说白了就是第一个有效的断点到底应该打在哪里。

你用前面我教的方法,也能把 @Cacheable 和 @Async 都玩明白。因为它们的底层逻辑和 @Transactional 是一样的。

所以,现在布置两个作业。

拿着这套组合拳,去上手玩一玩 @Cacheable 和 @Async 吧,沉淀出属于自己的方法论。

@Cacheable:

@Async:

最后,再附上几个我之前写过的文章,里面也用到了前面提到的几个方法定位源码,老舒服了。有兴趣可以看看:

《我是真没想到,这个面试题居然从11年前就开始讨论了,而官方今年才表态。》

《确实很优雅,所以我要扯下这个注解的神秘面纱。》

《关于Request复用的那点破事儿。研究明白了,给你汇报一下。》

《千万千万不要在方法上打断点!太坑了!》

好了,本文就到这里啦。如果你觉得对你有一丝丝帮助的话,求个免费的赞,不过分吧?

以上是关于教会你的主要内容,如果未能解决你的问题,请参考以下文章

教会你

在终端上用 C++ 简单编写一个的猜谜游戏!

C/C++游戏开发教程:C语言实现—飞翔的小鸟

前端入行三年--教会了我这些道理

如何用C语言开发图形化游戏

我想开发一款网络游戏,请问有啥语言写客户端,用啥语言写服务器端