2023年12月

$0 和 __vue__

$0
是指当鼠标点击 Element 面板的某个 dom 元素后,console 里 $0 变量会自动指向该 dom 元素对象

__vue__
是指 vue 框架会往 vue 组件 $mount 挂载的 dom 元素对象上添加一个
__vue__
变量来指向当前 vue 组件

这意味我们可以直接在 console 面板里拿到任意 vue 组件实例对象:

  • 可以查看对象内部任意属性
  • 也可以直接操作对象内部的属性来达到预期的调试效果
  • 甚至可以用来查看三方库的 api,比如 element-ui 的某个组件内部的 api

或许你会疑问,这不是安装 chrome 插件(vue devtools)就可以搞定的事吗,插件还是个可视化界面操作,更直观便捷

没错!插件当然更方便,但没准插件内部实现原理就是这样的呢,掌握这个技巧,也可以在一些插件无法覆盖的场景下来调试页面,比如:

  • 生产环境的页面
  • 内网部署且无法代理到本地的页面
  • 非 chrome 浏览器
  • 等等

举一反三:

  • 很多开源库框架其实都会往绑定的 dom 或者 window 上挂载一些变量上去,善于在 console 利用这些变量,可以方便我们进行很多非本地开发场景下的调试
  • 我们日常封装一些复杂业务、复杂组件时,也可以参考这种思路,给自己开一个非本地开发场景下的调试入口,但注意别内存泄漏了
  • window 上挂实例对象变量容易导致内存泄漏,所以建议挂一些全局作用域的对象;实例对象尽量挂与他生命周期绑定的 dom 对象

接口数据搜索定位

网络面板支持多个维度的搜索功能:

  • 根据内容关键词定位接口
    • 常用于看见界面某个文案或者只知道某个关键词,但想定位它是哪个接口返回时的场景
  • 在接口返回的内容里定位关键词
  • 根据 url 过滤接口

源码定位

jquery 时代的网页源码直接原原本本在浏览器上,调试和阅读都非常方便;

前端工程化后,混淆和压缩已经是标配,当出现生产故障时,首先考虑的应该是本地复现或者是将生成环境页面代理至本地的思路来解决;
但总会有某些场景,由于各种受限,只剩下浏览器直接操作的手段,因此,掌握一些源码定位和调试的方式技巧还是有必要的

全局搜索

字符串、对象属性字段名这些不会被混淆,可以借助这类场景的关键词来搜索定位源码位置
通常界面也是第一看到的东西,尽量找个界面上看着不像通用类的文案全局搜索下,基本都能定位到对应组件源码

也可以审查元素,在 Element 面板里找到对应 dom 上比较唯一的 class 或 id 等来全局搜索

接口调用栈

接口也是逻辑分析的入口点之一,比如找到某个页面呈现数据的请求接口,跟着调用栈走下去基本能梳理界面从拿到数据到呈现做了什么事
但要注意过滤掉三方库(如 axios, vue 等)对接口的封装,找到真实属于逻辑源码的调用栈

console 面板的函数源码跳转

console 面板上的 log 日志,或者通过 log 输出一个函数,都支持点击跳转到对应源码位置
借助
$0 和 __vue__
变量,找到对应组件的某个方法入口,通过 log 输出方法再跳转至源码位置,就能针对性的梳理某个逻辑源码

事件监听事件跳转

对于某些按钮等组件的点击之类的事件,可以直接通过审查元素的 Element 面板的 EventListeners(事件监听器)这边查看到该按钮各类事件的监听器
过滤掉三方库的统一事件监听器,找到自己逻辑源码的事件监听器入口,也能针对性梳理该事件处理逻辑源码

源码调试

断点

代码里加入 debugger 可以触发断点,但前提是本地开发调试模式
也可以直接浏览器上操作断点,除了常规的直接点击源码断点之外,也可以使用一些具体场景的断点,比如定时器触发时进入断点、异步请求响应时断点,DOM 变更时断点等等

overrides(覆盖)

如果想在浏览器上修改源码并生效的话,需要使用到 overrides(覆盖|替换)功能,如上图
先在 Source(源代码/来源) 面板启用 overrides(替换) 功能,然后找到要编辑的源码文件,右键,点击替换内容(Save for override)
接下去就可以直接浏览器上修改源码,刷新后会加载这份修改后的代码文件

当你发现网络面板有个感汗号警告时,就意味着你启用了本地替换的功能,指定的源码文件不会从网络上加载,而是加载本地临时替换修改过的文件
这样就可以达到直接在源码调试的效果

实操场景说明

以上是个人日常比较常用的一些操作,每个技巧都不是割裂开的,经常是组合交叉使用

比如某生产环境来了个故障,本地无法复现,无法代理,还是个内网,只能远程客户设备直接在浏览器界面上调试定位

首先,我们要定位到大概源码,然后分析、断点、调试

怎么定位源码呢:

  • 字符串是不会被混淆的,所以可以全局搜索方式来尝试
  • 如果匹配点太多,那也可以先定位到某个关键接口,然后根据接口定位到源码
  • 如果还是没有定位到,对象的属性是不会被混淆的,所以也可以通过
    $0 和 __vue__
    来根据某个 vue 组件的方法来定位源码

定位到源码后,断点还分析不出问题时,就可以继续利用
overrides
覆盖功能来直接修改浏览器源码进行调试

一、本次测试目的


基于QT环境下STM32人体红外检测,实现客户端红外采集到信息向服务端通信。

二、功能

(1)、传入音乐,当服务端接收到信息时,打开音乐

(2)、在服务端上面显示图片,当接收到打开或者关闭信息时,切换图片

三、代码展示


【服务端】
文件名称:untitledReceivingInformation


1、右键点击服务端文件夹,单击添加新文件。

2、在弹出的选择模板中选择Qt,选择Qt Resource File,单击Choose... 创建

3、输入添加新文件的名称 picture 单击下一步,单击完成。

4、因此项目文件untitledReceivingInformation路径下,出现资源文件路径,添加的新文件 picture.qrc 会自动生成到资源目录下面。

5、在picture.qrc 中单击添加列表,单击添加前缀,删除下方属性栏中前缀的内容。

【这里面的前缀是已经删除过原有的前缀】

6、点击添加列表,单击添加文件,会自动弹出存放untitledReceivingInformation文件的界面。因此我需要将图片保存到当前的文件夹。【以1.jpg为例】

头文件:widget.h

1 #ifndef WIDGET_H2 #define WIDGET_H
3 
4 #include <QWidget>
5 #include <QTcpServer>
6 #include <QTcpSocket>
7 #include <QHostAddress>
8 #include <QMediaPlayer>
9 #include <QPixmap>
10 
11 namespaceUi {12 classWidget;13 }14 
15 class Widget : publicQWidget16 {17 Q_OBJECT18 
19 public:20     explicit Widget(QWidget *parent = 0);21     ~Widget();22 privateslots:23     //写一个槽函数24     //用于接收建立连接
25     voidslotNewConn();26 
27     //接收到信息后进行显示
28     voidslotRecv();29     //声音暂停按钮
30     voidon_pushButton_clicked();31     //手写槽,播放音乐
32     voidopenmusic();33     //手写槽,关闭音乐
34     voidclosemusic();35     //开灯的图片响应
36     voidturn_on_the_light();37     //关灯的图片响应
38     voidclose_on_the_light();39 
40 private:41     Ui::Widget *ui;42     //声明server、socket指针变量
43     QTcpServer *server;44     QTcpSocket *socket;45 
46 QPixmap pix;47 QMediaPlayer player;48     intcount;49 };50 
51 #endif //WIDGET_H

源文件:widget.cpp

1 #include "widget.h"
2 #include "ui_widget.h"
3 #include "QPixmap"
4 #include "QDir"
5 
6 Widget::Widget(QWidget *parent) :7 QWidget(parent),8     ui(newUi::Widget)9 {10     ui->setupUi(this);11     //计算函数
12     count = 1;13     this->setWindowTitle("服务端");14     //给server指针开辟内存
15     server = new QTcpServer(this);16     //监听是否收到客户端的信息
17     server->listen(QHostAddress::AnyIPv4,9999);18 
19     //提前约定20     //对手写槽进行连接
21 connect(server,22 SIGNAL(newConnection()),23             this,24 SLOT(slotNewConn()));25 
26     //将当前的图片保存到path中
27     QString path =QDir::currentPath();28     //连接音乐
29     ///Jingle Bells.mp3
30     player.setMedia(QUrl(path + "/1.wav"));31 
32 }33 
34 Widget::~Widget()35 {36 delete ui;37 }38 /**39 * @brief Widget::slotNewConn40 * 手写的槽函数用于接收是否连接到网络41  */
42 voidWidget::slotNewConn(){43 
44     if(server->hasPendingConnections()){45         socket = server->nextPendingConnection();46         //如果显示输出,则连接建立成功47         //在label上面显示
48         ui->label->setText("有客户端来了");49 
50         //手写槽需要建立连接 connect【接收】
51         connect(socket,//信号的发出者
52                 SIGNAL(readyRead()),//读消息
53                 this,54 SLOT(slotRecv()));55 
56 }57 }58 
59 /**60 * @brief Widget::slotRecv61 * 在服务器内接收客户端的信息62 * 没有按钮来点击接收,因此需要添加一个 手写槽slotRecv()63 * 用来接收人体传感器的信息64  */
65 voidWidget::slotRecv(){66 
67     //在服务器内接收客户端的信息68     //没有按钮来点击接收,因此需要添加一个 手写槽slotRecv()【类型于自己手写一个显示方法】69     //现在接收的类型是 QByteArray 类型
70 QString str;71 QByteArray array;72 
73     //readAll()  读取所有的东西
74     array = socket->readAll();75 
76     //调用append函数【添加】,将类型转换成QString类型
77 str.append(array);78 
79     //然后在label_2上面显示字符串类型的内容
80     ui->label_2->setText(str);81     if(str == "infared_on\r\n"){82         //如果接收的信息是"infared_on"83         //那么我就在label_3上面显示开灯
84         ui->label_3->setText("开灯");85         //调用手写槽函数86         //如果是开灯,就播放这张照片
87 turn_on_the_light();88         //这个音乐
89 openmusic();90         //在这个写这段代码,有一个弊端,接收点一下发送消息,才会响应一次91         //【需要:当我发送的是开灯信号的时候,声音是一直播放的】92         //如果接收到开灯的信号,那么将播放音乐93         //player.play();94         //如果接收到开灯的信号,那么将把照片放置到label_4中95         //pix.load(":/1.jpg","jpg");96         //ui->label_4->setPixmap(pix);97         //将图片进行自适应98         //ui->label_4->setScaledContents(true);99 
100         //定义一个手写槽
101 
102     }else{103 
104         ui->label_3->setText("关灯");105         //如果接收到关灯的信号,那么将暂停音乐106         //player.pause();107         //调用手写槽,如果是关灯,就换另一种图片
108 close_on_the_light();109 closemusic();110 }666666 }112 /**113 * @brief Widget::on_pushButton_clicked114 * 手动暂停按钮115  */
116 voidWidget::on_pushButton_clicked()117 {118     //当按钮被按下,音乐会被暂停【手动暂停】119     //如果接收到开灯的信息,不想播放声音,可以手动暂停
120 closemusic();121 }122 /**123 * @brief Widget::openmusic124 * 手写槽打开音乐125  */
126 voidWidget::openmusic(){127     //【修复一个只有在点击的情况下,才会播放音乐】128     //【让音乐持续播放】129     //修复音乐无限的bug,原因是while的判断条件内,count初始不能为0
130     while(count){//上面代码块中定义的count = 1
131 player.play();132         if(count == 5){133             return;134 }135         count++;136 }137 
138 }139 
140 /**141 * @brief Widget::closemusic142 * //手写槽,关闭音乐143  */
144 voidWidget::closemusic(){145 
146 player.pause();147 
148 }149 /**150 * @brief Widget::turn_on_the_light151 * 开灯的图片响应152  */
153 voidWidget::turn_on_the_light()154 {155     pix.load(":/2.jpg","jpg");156     ui->label_4->setPixmap(pix);157     ui->label_4->setScaledContents(true);158 }159 /**160 * @brief Widget::close_on_the_light161 * 关灯的图片响应162  */
163 voidWidget::close_on_the_light()164 {165     pix.load(":/1.jpg","jpg");166     ui->label_4->setPixmap(pix);167     ui->label_4->setScaledContents(true);168 }

界面文件:widget.ui

界面文件中对象组件:


【服务端】文件名称:untitledTransmission


头文件:widget.h

1 #ifndef WIDGET_H2 #define WIDGET_H
3 
4 #include <QWidget>
5 #include <QTcpServer>
6 #include <QTcpSocket>
7 #include <QSerialPort>
8 #include <QMessageBox>
9 
10 namespaceUi {11 classWidget;12 }13 
14 class Widget : publicQWidget15 {16 Q_OBJECT17 
18 public:19     explicit Widget(QWidget *parent = 0);20     ~Widget();21 
22 privateslots:23     //建立连接按钮
24     voidon_pushButton_clicked();25     //是否连接成功
26     voidslotCostomer();27     //发送信息  按钮
28     voidon_pushButton_4_clicked();29     //串口按钮
30     voidon_pushButton_5_clicked();31     //创建的手写槽32     //读取消息
33     voidslotRecvSerial();34     //建立手写槽自动给服务端发送信息
35     voidAutomatically_send_messages();36 
37 private:38     Ui::Widget *ui;39 
40     //创建一个socket的指针变量
41     QTcpSocket *socket;42     //创建一个serial的指针变量
43     QSerialPort*serial;44 
45 };46 
47 #endif //WIDGET_H

源文件:widget.cpp

1 #include "widget.h"
2 #include "ui_widget.h"
3 
4 Widget::Widget(QWidget *parent) :5 QWidget(parent),6     ui(newUi::Widget)7 {8     ui->setupUi(this);9     this->setWindowTitle("客户端");10 
11     //为这个创建的指针变量分配空间
12     socket = new QTcpSocket(this);13     //手写槽建立连接
14 connect(socket,15 SIGNAL(connected()),16             this,17 SLOT(slotCostomer()));18 
19     //检测红外信号接收【采集人体传感器信息,然后进行发送】20     //给指针分配内存
21     serial = new QSerialPort(this);22 
23     //手写槽创建连接
24 connect(serial,25 SIGNAL(readyRead()),26             this,27 SLOT(slotRecvSerial())28 );29 
30     //建立自动连接发送信息
31 connect(serial,32 SIGNAL(readyRead()),33             this,34 SLOT(Automatically_send_messages())35 );36 
37 
38 }39 
40 Widget::~Widget()41 {42 delete ui;43 }44 
45 /**46 * @brief Widget::on_pushButton_clicked47 * 这个按钮是建立连接48  */
49 voidWidget::on_pushButton_clicked()50 {51 QString ipaddr,port;52     //获取IP地址的内容53     //lineEdit 是IP地址
54     ipaddr = ui->lineEdit->text();55     //获取端口号的内容56     //lineEdit_2 是端口号
57     port = ui->lineEdit_2->text();58 
59     intm_port;60     //将端口号转换成int类型
61     m_port =port.toInt();62 
63     //cmd  ipconfig/all64     //ipv4   192.168.66.17965     //将这个程序进行发送
66     socket->connectToHost(ipaddr,m_port);67 }68 
69 /**70 * @brief Widget::slotCostomer71 * 这个连接是不需要按钮触发的,因此需求在加载的时候触发连接72  */
73 voidWidget::slotCostomer(){74     //label  是显示连接信息
75     ui->label->setText("连接服务器成功...");76 }77 
78 //192.168.87.9579 //下面的代码是发送消息按钮
80 /**81 * @brief Widget::on_pushButton_4_clicked82 * 如果点击发送按钮,人体传感器的消息才会发送给服务端83 *84 * 【新问题】如果不借助按钮事件装置,将检测到人体传感器的信息直接发送到服务端85 * 1、应该将按钮装置设置成一个手写槽86 * 2、将手写槽建立自动连接87  */
88 /*
89 void Widget::on_pushButton_4_clicked()90 {91 QByteArray array;92 QString str;93 
94 //获取lineEdit_3的内容95 //内容是任何获取的呢?96 //【是我检测到人体传感器然后获取人体传感器的内容进行转发】97 str = ui->label->text();98 
99 //发送的时候只能发送QByteArray类型100 //因此调用append方法【增加】,把str追加到QSring后面,实现类型转换101 array.append(str);102 
103 //转换完成后就要进行发送104 //上方代码块中已经在内存中为socket开辟了空间105 socket->write(array);106 }107 */
108 /**109 * @brief Automatically_send_messages()110 * 建立自动连接发送信息666666  */
112 voidWidget::Automatically_send_messages(){113 
114 QByteArray array;115 QString str;116 
117     //获取lineEdit_3的内容118     //内容是任何获取的呢?119     //【是我检测到人体传感器然后获取人体传感器的内容进行转发】
120     str = ui->label->text();121 
122     //发送的时候只能发送QByteArray类型123     //因此调用append方法【增加】,把str追加到QSring后面,实现类型转换
124 array.append(str);125 
126     //转换完成后就要进行发送127     //上方代码块中已经在内存中为socket开辟了空间
128     socket->write(array);129 }130 
131 /**132 * @brief on_pushButton_5_clicked()133 * 串口按钮134  */
135 voidWidget::on_pushButton_5_clicked()136 {137 QString strPortName,strBaudRate;138     //获取comboBox的数据
139     strPortName = ui->comboBox->currentText();140     strBaudRate = ui->comboBox_2->currentText();141     serial->setPortName(strPortName);142     serial->setBaudRate(strBaudRate.toInt());143 
144     //设置数据位145     //枚举类型
146     serial->setDataBits(QSerialPort::Data8);147     //设置停止位
148     serial->setStopBits(QSerialPort::OneStop);149     //校验位
150     serial->setParity(QSerialPort::NoParity);151     //152     serial->setFlowControl(QSerialPort::NoFlowControl);153 
154     //打开
155     boolok;156     ok = serial->open(QIODevice::ReadWrite);157     if(ok == true){158         QMessageBox::information(this,"打开串口","串口打开成功");159     }else{160         QMessageBox::warning(this,"打开串口","串口打开失败");161 }162 }163 /**164 * @brief Widget::slotRecvSerial165 * 创建的手写槽166 * 读取消息167  */
168 voidWidget::slotRecvSerial(){169 
170     //读取获取人体传感器此时的消息
171 QByteArray array;172 QString str;173     //全部读取【人体传感器此时的信息】
174     array = serial->readAll();175     //类型转换
176 str.append(array);177     //读取的消息,我直接在label上面显示出来【】
178     ui->label->setText(str);//当我点击发送消息的按钮时,label上面显示的人体传感器的消息会发送给服务端
179 }

界面文件:widget.ui

界面文件中对象组件:

四、代码测试

引言

哈喽。大家好,好久不见,最近遇到了一个场景,就是在FrameWork的asp.net mvc中,有个系统里面使用的是EntityFramework的框架,在这个框架里,提供了一个SqlQuery的方法,这个方法很好用啊,以至于在EFCORE8里面又添加了回来,不过不知道性能怎么样,我遇到的场景是通过SqlQuery查询的时候,转换很慢,我估计那背后大概率是使用反射造成的, 因为我的查询可能有上十万,甚至更多,就导致了这个转换的过程及其耗时,以至于刚开始我是想通过Emit等方式去实现一个高性能转换,可是到最后没有去弄,因为我用了DataCommand去查询,最后循环DataReader来实现硬赋值,这样性能是最好,一下减少了好多秒,提升了80%,但也给了我一个灵感,一个实现简易的类型转换的灵感,所以在上周我就把代码写了出来,不过由于工作的忙碌,今天才开始写博客,接下来就呈上。

对了,有关EMIT和表达式树的知识,诸位可以看我之前的博客
表达式树
,
IL
。其中IL有两合集。

EMIT

众所周知,我们的c#代码在编译器编译,都会编译成IL代码,最后再去通过JIT转化为机器码,运行在系统中去的,所以IL代码的性能是比c#代码高的,同时,学习的成本,编写的成本也是机器高,我也是在自己感兴趣,瞎琢磨,就会了一丝丝皮毛,很多人说IL难写,其实,对于代码中的Opcodes那些,我只记一些常用的,对于剩下的,我都是在写的时候才去看文档,总之呢,要学的东西有很多,但掌握了学习的方法,才是持之以恒的手段。

接下来,就呈上IL代码,分为两部分,一个是List转List,一个是实体转实体的。

在这几个例子中,所有的前提都是实体的属性名称是一样的,如果需要扩展类型不一样,或者哪些不转换,从哪个属性转换到哪个属性,就需要各位自己去扩展了,本来我是想写这些的,,但是懒癌犯了,哈哈哈哈,需要各位看官自己动手了,以下代码,除了反射,其他的我都加了注释,反射大家都看得懂。

在下面的第一个方法,我们定义了执行转换集合的方法,并返回了一个委托,我们在实际开发中,都可以返回委托,最终可以将方法缓存起来,这样在后续的时候直接调用,性能提升爆炸,因为你每次创建Emit方法的时候,耗时也会挺长的,在有时候,像哪些主流的Mapper,他们内部肯定都做了缓存。下面的集合转集合,大致的原理代码就是定义一个方法ConvertToType,返回类型是List<TR>,入参是List<T>,然后定义循环的开始结束变量,以及最终返回结果集,还有循环内部的时候,我们创建的变量,最终将这个变量添加到返回的结果集中,在往下就是拿入参集合的数量,定义循环开始和结束的label,在往下走就是,初始化返回值集合,赋值给本地的localRes变量,将0赋值给开始循环的变量,也就是for(int i=0),下面就是给结束的循环值赋值为入参集合的Count。

下面的代码每行基本都有注释,所以我在这里也不做过多的讲解。

集合和单个的区别就在于集合是多了一个循环的主体,其他都和单个是一样的,以及集合的代码块中,我没有添加try catch的代码块。

    internal class EmitExecute<T, TR> : IExecute<T, TR> where T : class where TR : class, new()
{
public Func<List<T>, List<TR>>ExecuteList()
{
var dynamicMethod = new DynamicMethod("ConvertToType", typeof(List<TR>), new Type[] { typeof(List<T>) });#region 变量定义 var ilMethod =dynamicMethod.GetILGenerator();var localBegin = ilMethod.DeclareLocal(typeof(int));//定义循环开始变量 var localEnd = ilMethod.DeclareLocal(typeof(int));//结束变量 var localres = ilMethod.DeclareLocal(typeof(List<TR>));//返回值 var localSignleRes = ilMethod.DeclareLocal(typeof(TR));//变量 var countMethod = typeof(List<T>).GetProperty("Count").GetGetMethod();#endregion var labelXunhuan = ilMethod.DefineLabel();//定义循环主体 var labelEnd = ilMethod.DefineLabel();//定义结束标签主体 #region 实例化返回值ilMethod.Emit(OpCodes.Nop);
ilMethod.Emit(OpCodes.Newobj,
typeof(List<TR>).GetConstructor(Type.EmptyTypes));//初始化返回值变量 ilMethod.Emit(OpCodes.Stloc, localres);//结果赋值给返回值变量变量 #endregion #region 给循环的起始变量赋值 0是开始,count是获取的入参的集合的countilMethod.Emit(OpCodes.Ldc_I4_0);//将0加载到栈上 ilMethod.Emit(OpCodes.Stloc, localBegin);//给循环的开始值begin赋值0 ilMethod.Emit(OpCodes.Ldarg_0);//加载入参 ilMethod.Emit(OpCodes.Callvirt, countMethod);//将入参的count加载到栈上 ilMethod.Emit(OpCodes.Stloc, localEnd);//给结束变量赋值为集合的count ilMethod.Emit(OpCodes.Br, labelXunhuan);//无条件跳转到循环的label,即开始循环 #endregion #region 循环结束标签ilMethod.MarkLabel(labelEnd);//标记这段代码是labelend的代码 ilMethod.Emit(OpCodes.Ldloc, localres);//加载返回值变量 ilMethod.Emit(OpCodes.Ret);//返回 #endregion #region 循环主体ilMethod.MarkLabel(labelXunhuan);//标记是labelbegin的代码 ilMethod.Emit(OpCodes.Ldloc, localBegin);//从栈中加载开始变量 ilMethod.Emit(OpCodes.Ldloc, localEnd);//从栈中加载结束变量 ilMethod.Emit(OpCodes.Bge, labelEnd);//比较第0个是否小于等于第一个//ilMethod.Emit(OpCodes.Call, typeof(List<People>).GetMethod("Add")); ilMethod.Emit(OpCodes.Newobj, typeof(TR).GetConstructor(Type.EmptyTypes));//创建people的实例化 ilMethod.Emit(OpCodes.Stloc, localSignleRes);//结果赋值people变量 var properties = typeof(T).GetProperties().Where(s => typeof(TR).GetProperties().Any(a => a.Name ==s.Name)).ToList();if (properties != null)
{
foreach (var item inproperties)
{
var getMethod = typeof(T).GetProperty(item.Name).GetGetMethod();//获取list【i】的species属性 var setMethod = typeof(TR).GetProperty(item.Name).GetSetMethod();
ilMethod.Emit(OpCodes.Ldloc, localSignleRes);
//加载定义好的people ilMethod.Emit(OpCodes.Ldarg_0);//加载入参 ilMethod.Emit(OpCodes.Ldloc, localBegin);//加载循环的开始变量 ilMethod.Emit(OpCodes.Callvirt, typeof(List<T>).GetMethod("get_Item"));//调用 List<Animal>.get_Item 方法,将结果压入栈顶 ilMethod.Emit(OpCodes.Callvirt, getMethod);//拿到getitem的返回值的species属性 ilMethod.Emit(OpCodes.Call, setMethod);//给people的species属性赋值 }
}
//ilMethod.Emit(OpCodes.Ldloc, localPeople);//加载定义好的people//ilMethod.Emit(OpCodes.Ldarg_0);//加载入参//ilMethod.Emit(OpCodes.Ldloc, localBegin);//加载循环的开始变量//ilMethod.Emit(OpCodes.Callvirt, typeof(List<T>).GetMethod("get_Item"));//调用 List<Animal>.get_Item 方法,将结果压入栈顶//ilMethod.Emit(OpCodes.Callvirt, getAMethod);//拿到getitem的返回值的species属性//ilMethod.Emit(OpCodes.Call, property);//给people的species属性赋值 ilMethod.Emit(OpCodes.Ldloc, localres);//加载返回值 ilMethod.Emit(OpCodes.Ldloc, localSignleRes);//加载people ilMethod.Emit(OpCodes.Call,typeof(List<TR>).GetMethod("Add"));//将people添加道返回值 ilMethod.Emit(OpCodes.Ldc_I4_1);//将1加载到栈上 ilMethod.Emit(OpCodes.Ldloc, localBegin);//从栈中加载开始变量 ilMethod.Emit(OpCodes.Add);//相加 ilMethod.Emit(OpCodes.Stloc, localBegin);//结果赋值给本地0个局部变量 ilMethod.Emit(OpCodes.Br, labelXunhuan);#endregion var func = dynamicMethod.CreateDelegate<Func<List<T>,List<TR>>>();returnfunc;
}
public Func<T, TR>ExecuteSingle()
{
var dynamicMethod = new DynamicMethod("ConvertToType", typeof(TR), new Type[] { typeof(T) });var method = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });//输出字符串 var ilMethod =dynamicMethod.GetILGenerator();var localRes = ilMethod.DeclareLocal(typeof(TR));//变量 var ex = typeof(Exception);var localEx =ilMethod.DeclareLocal(ex);var str = typeof(Exception).GetMethod("ToString");
ilMethod.Emit(OpCodes.Nop);
ilMethod.Emit(OpCodes.Newobj,
typeof(TR).GetConstructor(Type.EmptyTypes));//初始化返回值变量 new tr(); ilMethod.Emit(OpCodes.Stloc, localRes);//结果赋值给返回值变量变量var tr=new tr(); var properties = typeof(T).GetProperties().Where(s => typeof(TR).GetProperties().Any(a => a.Name ==s.Name)).ToList();if (properties != null)
{
ilMethod.BeginExceptionBlock();
//try foreach (var item inproperties)
{
var getMethod = typeof(T).GetProperty(item.Name).GetGetMethod();//获取list【i】的species属性 var setMethod = typeof(TR).GetProperty(item.Name).GetSetMethod();
ilMethod.Emit(OpCodes.Ldloc, localRes);
//加载定义好的people ilMethod.Emit(OpCodes.Ldarg_0);//加载入参 ilMethod.Emit(OpCodes.Callvirt, getMethod);//拿到getitem的返回值的species属性 ilMethod.Emit(OpCodes.Call, setMethod);//给people的species属性赋值 }
ilMethod.BeginCatchBlock(ex);
ilMethod.Emit(OpCodes.Stloc, localEx);// 将异常的ex赋值给我们的本地变量ex
ilMethod.Emit(OpCodes.Ldloc, localEx);//加载ex变量
ilMethod.Emit(OpCodes.Callvirt, str);//调用tostring()
ilMethod.Emit(OpCodes.Call, method);//调用console.writeline方法打印出异常信息
ilMethod.Emit(OpCodes.Newobj,
typeof(TR).GetConstructor(Type.EmptyTypes));
ilMethod.Emit(OpCodes.Stloc, localRes);
//结果赋值给返回值变量变量 ilMethod.EndExceptionBlock();
}
ilMethod.Emit(OpCodes.Ldloc, localRes);
//加载返回值 ilMethod.Emit(OpCodes.Ret);var func = dynamicMethod.CreateDelegate<Func<T, TR>>();returnfunc;
}
}
}

Expression

接下来,是表达式树的实现方式,表达式树的其实和Emit的我感觉都差不多,不过和emit相比,肯定大家都喜欢写Expression,毕竟是c#代码,写起来比较舒适,在下面代码就是定义了入参的source,以及从source那指定索引的item,以及返回值res,异常的定义和异常的message,在下面就是循环两个公共属性的信息,调用bind方法,从item的里面拿出sourceproperty的属性和targetproperty绑定,然后给res初始化,设置他的count为source的count,并且判断如果source长度是0,就直接返回一个空的集合,下面有一个构造循环的方法,判断index是否小于集合的count,如果不成立,直接调用break标签,也就是我们的break关键字,如果成立,拿出对应的item,然后调用了MemberInit方法,初始化了一个TR,然后调用Add方法添加到返回的结果集合中,这样就实现了一个一个的转换,最后将所有的表达式整合为一个代码块,通过Block再加入Try Catch,最终编译成一个Func的委托。下面的单个的原理也是一样的。

  internal class ExpressionExecute<T, TR> : IExecute<T, TR> where T : class where TR : class, new()
{
private List<PropertyInfo>properties;publicExpressionExecute()
{
properties
= typeof(TR).GetProperties().Where(s => typeof(T).GetProperties().Any(a => a.Name == s.Name)).ToList();//获取相同的属性名称 }public Func<List<T>, List<TR>>ExecuteList()
{
var sourceParam = Expression.Parameter(typeof(List<T>), "source");//定义入参 var itemVar = Expression.Variable(typeof(T), "item");//定义从集合获取的item变量 var resVar = Expression.Variable(typeof(List<TR>), "res");//定义返回的结果变量 var ex = Expression.Variable(typeof(Exception), "ex");var msg = Expression.Property(ex, "Message");var method = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });var memberBindings = new List<MemberBinding>();//memberbind var listExpressions = new List<Expression>();//expression foreach (var property inproperties)
{
var sourceProperty = typeof(T).GetProperty(property.Name);var targetProperty = typeof(TR).GetProperty(property.Name);if (sourceProperty.PropertyType == targetProperty.PropertyType)//判断类型 {
memberBindings.Add(Expression.Bind(targetProperty, Expression.Property(itemVar, sourceProperty)));
//相等直接bind add }
}

listExpressions.Add(Expression.Assign(resVar, Expression.New(
typeof(List<TR>))));//设置res=new list<tr> listExpressions.Add(Expression.Assign(Expression.Property(resVar, "Capacity"), Expression.Property(sourceParam, "Count"))); //设置res.count==入参的count var indexVar = Expression.Variable(typeof(int), "index");//循环索引 var breakLabel = Expression.Label("LoopBreak");//break标签 listExpressions.Add(Expression.IfThen(
Expression.Equal(Expression.Property(sourceParam,
"Count"), Expression.Constant(0)),//如果source.cout==0 Expression.Return(breakLabel, resVar, typeof(List<TR>)) //直接 break 返回res ));//构造循环 var innerLoop =CreateLoop(indexVar, sourceParam, itemVar,resVar,memberBindings,breakLabel);
listExpressions.AddRange(
newExpression[] { innerLoop , Expression.Label(breakLabel) , resVar });var body = Expression.Block(new[] { itemVar, indexVar, resVar }, listExpressions);//整合一个代码块 var tryCatch = Expression.TryCatch(body, new CatchBlock[] { Expression.Catch(ex, Expression.Block(Expression.Call(null, method, msg), Expression.New(typeof(List<TR>)))) });//try catch var lambda = Expression.Lambda<Func<List<T>, List<TR>>>(tryCatch, sourceParam);returnlambda.Compile();
}
private LoopExpression CreateLoop(ParameterExpression indexVar,ParameterExpression sourceParam,ParameterExpression itemVar,ParameterExpression resVar,List<MemberBinding>memberBindings,LabelTarget breakLabel)
{
var addMethod = typeof(List<TR>).GetMethod("Add");//结果集的add方法 returnExpression.Loop(
Expression.IfThenElse(
//如果index <count 进入block Expression.LessThan(indexVar, Expression.Property(sourceParam, "Count")),
Expression.Block(
Expression.Assign(itemVar, Expression.Property(sourceParam,
"Item", indexVar)),//设置item=source【index】 Expression.Call(resVar, addMethod, Expression.MemberInit(Expression.New(typeof(TR)), memberBindings)),//调用res.add方法 Expression.PostIncrementAssign(indexVar)//index=index+1 ),
Expression.Break(breakLabel)
//如果index<count不成立,直接中断循环 )
);
}
public Func<T, TR>ExecuteSingle()
{
var express = Expression.Parameter(typeof(T), "source");var memberBindings = new List<MemberBinding>();//memberbing var list = new List<Expression>();var action = new Func<string, string>(s =>{return typeof(TR).GetProperty(s).Name;
});
//根据属性名称获取属性,由于property第二个参数必须 string or method,下方就只有他通过call的方式获取属性的name名称 int ipropertydx = 0;//相同属性遍历所以 memberBindings.Clear();var peoples = Expression.New(typeof(TR));//创建新的tr实例 foreach (var item in properties)//属性遍历 {var property = Expression.Property(peoples, item.Name);//获取tr实例属性 var memberInfo = typeof(TR).GetMember(item.Name).FirstOrDefault(); //获取 MemberInfo 对象 if (memberInfo != null)
{
var assignment = Expression.Bind(//将属性和source[index].属性关联起来 memberInfo, Expression.Property( //获取source的属性 express,//调用上面的委托返回要拿的属性名称也就是A.name=B.name Expression.Lambda<Func<string>>(
Expression.Call(Expression.Constant(action.Target), action.Method,
//调用action拿名称 Expression.Property(
Expression.ArrayIndex(Expression.Constant(properties.ToArray()), Expression.Constant(ipropertydx)),
"Name")//获取properties的第ipropertydx的name名称 )
).Compile()
//编译为func《string》委托 ()
));
memberBindings.Add(assignment);
//将每个people的每个属性赋值的assignment添加进去 ipropertydx++;
}
}
var memberInit = Expression.MemberInit(peoples, memberBindings);//将peoples初始化 初始化绑定的每一个成员 var func = Expression.Lambda<Func<T, TR>>(memberInit, express).Compile();//编译为委托 returnfunc;
}
}

反射

反正,反射是很耗时的,少量情况还好,大量并不建议使用,虽然很好用,这里我也只是做一个例子,让我自己用,肯定优选前面两个,这个代码更不用讲了,懂得都懂,

  internal class ReflectionExecute<T, TR> : IExecute<T, TR> where T : class where TR : class,new()
{
public Func<List<T>, List<TR>>ExecuteList()
{
var res = new Func<List<T>, List<TR>>(s =>{var resList = new List<TR>();var properties = typeof(T).GetProperties().Where(s => typeof(TR).GetProperties().Any(a => a.Name == s.Name)).ToList();//获取相同的属性名称 foreach (var item ins)
{
var tr = newTR();foreach (var itemproperty inproperties)
{
var val = itemproperty.GetValue(item, null);if (val != null)
{
var setProperty = typeof(TR).GetProperty(itemproperty.Name);
setProperty.SetValue(tr, val);
}
}
resList.Add(tr);
}
returnresList;
});
returnres;
}
public Func<T, TR>ExecuteSingle()
{
var res = new Func<T, TR>(s =>{var properties = typeof(TR).GetProperties().Where(s => typeof(T).GetProperties().Any(a => a.Name == s.Name)).ToList();//获取相同的属性名称 var tr = newTR();foreach (var itemproperty inproperties)
{
var val = itemproperty.GetValue(s, new object[] { });if (val != null)
{
var setProperty = typeof(TR).GetProperty(itemproperty.Name);
setProperty.SetValue(tr, val);
}
}
returntr;
});
returnres;
}
}

测试

这玩意,我自己简单测试了一下,本想用benmark简单跑一下,但是麻烦,就代码自己测试了一下,在第一次构建表达式树的方法,会有些许耗时,但是在最后如果有缓存方法,那性能不必Emit差,总之,性能方面优选Emit和表达式树,反射就不做考虑。

总结

赶鸭子上架,水了一篇博客,如有疑问,可以随时呼我,QQ934550201.

代码地址:

链接:https://pan.baidu.com/s/1vW9LPfYHmvk6Y08qEYA9sw
提取码:vj34

最近受废话文学的影响,所以有了今天的这个标题,希望大家能喜欢。大家不喜欢也没关系,反正我喜欢。

鲁迅先生曾经说过“我家门前有两棵树,一棵是枣树,另一棵还是枣树。”,由此可见,这老爷子算是把废话文学给玩明白了。

希望大家也能,听君一席话,如听一席话吧。

聊正题

聊完闲篇,咱们说回正题:
什么是意向锁?为什么需要意向锁?

PS:这里没意向书什么事啊,标题中有,纯粹是为了好玩。他们不能说没有什么关系,那是一点关系都没有。

定义

意向锁(Intention Locks)是 MySQL InnoDB 引擎中的一种锁机制(表级锁),用于协调事务间的加锁操作,以避免冲突和死锁的发生。

为什么要有意向锁?

例如,事务 A 加锁了 Users 表中的一行数据(行锁),而事务 B 要对整个 Users 表进行加锁(表锁),那么这个时候,如果没有意向锁,那事务 B 就会加锁成功了。而事务 B 如果加锁成功的话,那么它是可以对表中的任意数据行进行操作的,包括事务 A 加锁的那行数据,所以,这个时候就发生了锁冲突。

为了避免这个问题,所以 MySQL 引入了意向锁,在事务 A 添加行锁的时候,先添加意向锁,而事务 B 在添加表锁的时候,先判断一下意向锁,如果有意向锁了,它就不能加表锁了,这样避免了锁冲突,提升了加锁判断的效率。

想想一下,如果没有意向锁的话,那么事务 B 在加表锁的时候,只能一行行的判断有没有行锁,而这种判断的效率是非常低的,尤其数据量比较大时。

意向锁分类

意向锁分为以下两种类型:

  1. 意向共享锁(Intention Shared lock,IS)
    :表示在某个资源上设置共享锁。也就是读锁,用于读取数据的操作,允许多个事务同时持有(共享锁),不互斥。
  2. 意向排他锁(Intention Exclusive lock,IX)
    :表示在某个资源上设置排他锁。也就是写锁,用于修改和更新数据操作,并且同一时间只能由一个事务持有,不能和其他事务共同持有,具有互斥性和排他性。

小结

意向锁并不能直接由开发者创建和使用,它是存在于 MySQL 中,由 MySQL 自行维护的一种协调事务加锁的机制。它在事务创建行锁和表锁时创建,在事务提交或回滚之后自动释放。

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。