gorm源码解析(四):事务,预编译
文章目录
- 前言
- 事务
- 自己控制事务
- 用 Transaction方法包装事务
- 预编译
- 事务结合预编译
- 总结
前言
前几篇文章介绍gorm的整体设计,增删改查的具体实现流程。本文将聚焦与事务和预编译部分
事务
自己控制事务
用gorm框架,可以自己控制事务的Begin,Commit和Rollback,如下所示:
// 开始事务
tx := db.Begin()// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...) // ... // 遇到错误时回滚事务
tx.Rollback()// 否则,提交事务
tx.Commit()
下面看看每个api的源码实现
Beigin:
- 新建一个db实例,作为本次事务操作的会话
- 非预编译模式下:调sql.DB的BeginTx方法,让
tx.Statement.ConnPool持有其返回的sql.Tx- 之后的增删改查操作,都用这个sql.Tx执行,会用同一个db连接(也就是调begin的那个连接)
预编译模式本文后面再介绍,这里只关注非预编译模式
func (db *DB) Begin(opts ...*sql.TxOptions) *DB { var ( // clone statement // 新建的tx.clone = 1 tx = db.getInstance().Session(&Session{Context: db.Statement.Context, NewDB: db.clone == 1}) opt *sql.TxOptions err error ) if len(opts) > 0 { opt = opts[0] } switch beginner := tx.Statement.ConnPool.(type) { // 非预编译模式下: case TxBeginner: tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) // 预编译模式下: case ConnPoolBeginner: tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) default: err = ErrInvalidTransaction } if err != nil { tx.AddError(err) } return tx
}
Commit和Rollback:调sql.Tx的Commit和Rollback方法,内部会调mysql驱动mysqlTx的Commit和Rollback方法,完成事务的提交和回滚操作
func (db *DB) Commit() *DB { if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil && !reflect.ValueOf(committer).IsNil() { db.AddError(committer.Commit()) } else { db.AddError(ErrInvalidTransaction) } return db
} func (db *DB) Rollback() *DB { if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil { if !reflect.ValueOf(committer).IsNil() { db.AddError(committer.Rollback()) } } else { db.AddError(ErrInvalidTransaction) } return db
}
用 Transaction方法包装事务
然而,自己控制事务有以下问题:
- 很多重复代码,样板代码
- 自己控制事务生命周期容易出问题,例如可能忘记commit或rollback,或者说发生panic但没有recover,导致没有触发commit或rollback
于是gorm提供了Transaction方法,帮我们调了Begin,事务执行成功后Commit,事务执行失败或发生panic时执行Rollback操作,也就是帮我们控制事务的生命周期,我们只用关注业务逻辑即可
使用方法为将业务逻辑func传进去,例如:
db.Transaction(func(tx *gorm.DB) error { // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db') if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { // 返回任何错误都会回滚事务 return err } if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { return err } // 返回 nil 提交事务 return nil
})

Transaction方法流程如下:
- db.Begin开启事务
- 执行fc,把事务tx传进去
- 如果fc执行成功,执行
tx.Commit提交事务 - 如果fc执行出错或发生panic,调
tx.Rollback回滚事务
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) {panicked := trueif committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil {// ...} else {// 开启事务tx := db.Begin(opts...)if tx.Error != nil {return tx.Error}defer func() {// 如果发生panic,或者执行出错,回滚if panicked || err != nil {tx.Rollback()}}()if err = fc(tx); err == nil {panicked = falsereturn tx.Commit().Error}}panicked = falsereturn
}
预编译
只要用database/sql配合mysql驱动,执行sql时一定走了预编译。要么是客户端预编译,要么是mysql服务端预编译
在gorm层面,PrepareStmt的目的是提高性能,而不是执行sql时从非预编译变成预编译
默认gorm层面的PrepareStmt为false,这里假设使用服务端预编译(连接mysql的dsn中interpolateParams为false),执行sql模板 + 参数类型的操作时有如下流程:
- 往mysql发送sql模板,获得
stmtId - 往msqyl发送stmtId + 参数,得到执行结果
- 往mysql发送释放stmt命令
如果使用gorm层面的PrepareStmt,会对sql.Stmt进行缓存,如果当前连接预编译过该Stmt,就能直接用
接下来看看gorm层面怎么处理预编译的
初始化db时(gorm.Open),如果config.PrepareStmt为true,使用预编译模式:
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {// ...// 预编译模式if config.PrepareStmt {preparedStmt := NewPreparedStmtDB(db.ConnPool)db.cacheStore.Store(preparedStmtDBKey, preparedStmt)db.ConnPool = preparedStmt}db.Statement = &Statement{DB: db,ConnPool: db.ConnPool,Context: context.Background(),Clauses: map[string]clause.Clause{},}// ...return
}
ConnPool被替换成PreparedStmtDB,其结构如下:
type PreparedStmtDB struct {// key: sql模板,value:StmtStmts map[string]*StmtMux *sync.RWMutex// 内置的 ConnPool 字段通常为 database/sql 中的 *DBConnPool
}
初始化PreparedStmtDB,内部持有sql.DB
func NewPreparedStmtDB(connPool ConnPool) *PreparedStmtDB {return &PreparedStmtDB{// 持有sql.DBConnPool: connPool,// sql到Stmt的映射Stmts: make(map[string]*Stmt),Mux: &sync.RWMutex{},}
}
可以看出PreparedStmtDB拥有sql模板到Stmt的缓存,那么遇到相同sql时,如果之前已经预编译过,就能用该Stmt执行db操作
后续基于这个db执行任何操作时,分为两个步骤:
- 通过
PreparedStmtDB.prepare(...)操作创建/复用 stmt,后续相同 sql 模板可以复用此 stmt - 通过
stmt.Query(...)/Exec(...)执行 sql

例如在执行PreparedStmtDB.QueryContext时:
- 先调PreparedStmtDB.prepare看有没有可复用的sql.Stmt
- 再调sql.Stmt执行QueryContext操作
func (db *PreparedStmtDB) QueryContext(ctx context.Context, query string, args ...interface{}) (rows *sql.Rows, err error) {// 先获取stmtstmt, err := db.prepare(ctx, db.ConnPool, false, query)if err == nil {// 再用stmt执行sqlrows, err = stmt.QueryContext(ctx, args...)if errors.Is(err, driver.ErrBadConn) {db.Mux.Lock()defer db.Mux.Unlock()go stmt.Close()delete(db.Stmts, query)}}return rows, err
}
PreparedStmtDB.prepare:
- 尝试从缓存Stmts中,根据query模板找sql.Stmt,如果有就返回
- 否则调sql.DB,根据query模板生成一个sql.Stmt,加入缓存中
func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransaction bool, query string) (Stmt, error) {db.Mux.RLock()// 以sql为模板,先查有没有可复用的stmt// 如果stmt.Transaction为false,可以复用if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) {db.Mux.RUnlock()// wait for other goroutines prepared<-stmt.preparedif stmt.prepareErr != nil {return Stmt{}, stmt.prepareErr}return *stmt, nil}db.Mux.RUnlock()db.Mux.Lock()// double checkif stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) {db.Mux.Unlock()// wait for other goroutines prepared<-stmt.preparedif stmt.prepareErr != nil {return Stmt{}, stmt.prepareErr}return *stmt, nil}// ...// 到这里没有可复用的模板// 创建stmt实例,加到map中cacheStmt := Stmt{Transaction: isTransaction, prepared: make(chan struct{})}db.Stmts[query] = &cacheStmtdb.Mux.Unlock()// prepare completeddefer close(cacheStmt.prepared)// 调sql.DB的prepareContext方法,创建sql.stmtstmt, err := conn.PrepareContext(ctx, query)if err != nil {cacheStmt.prepareErr = errdb.Mux.Lock()delete(db.Stmts, query)db.Mux.Unlock()return Stmt{}, err}db.Mux.Lock()cacheStmt.Stmt = stmtdb.Mux.Unlock()return cacheStmt, nil
}
这里是ORM层面对Stmt做了缓存
而在go标准库database/sql层面,stmt要能执行的前提是,在当前连接预编过
我们看sql.Stmt源码,注释上有这么一段话:
A Stmt is safe for concurrent use by multiple goroutines
翻译:Stmt能被多个g并发调用
When the Stmt needs to execute on a new underlying connection, it will prepare itself on the new connection automatically
翻译:当Stmt需要被新的连接使用时,需要在新连接上预编译
sql.Stmt有一个css slice,存放预编译了该Stmt的连接
type Stmt struct { // 持有driverStmtcgds *driverStmt // ...// 预编译了该Stmt的连接css []connStmt }type connStmt struct { dc *driverConn ds *driverStmt
}
基于Stmt执行Query, Exec操作时,会检查当前使用的连接在不在Stmt.css里面,如果在就能立即执行,否则需要先预编译该sql模板才能执行
这里以Stmt.ExecQuery为例,看看如何获取连接并执行
func (s *Stmt) ExecContext(ctx context.Context, args ...any) (Result, error) { s.closemu.RLock() defer s.closemu.RUnlock() var res Result err := s.db.retry(func(strategy connReuseStrategy) error {// 获取连接 dc, releaseConn, ds, err := s.connStmt(ctx, strategy) if err != nil { return err } // 执行execres, err = resultFromStatement(ctx, dc.ci, ds, args...) releaseConn(err) return err }) return res, err
}
重点在获取连接:
- 从连接池获取一个连接
- 检查该连接是否在Stmt.css里面,如果是,直接返回
- 否则需要先用该连接预编译sql模板
func (s *Stmt) connStmt(ctx context.Context, strategy connReuseStrategy) (dc *driverConn, releaseConn func(error), ds *driverStmt, err error) { // ...// 从连接池获取一个连接dc, err = s.db.conn(ctx, strategy) if err != nil { return nil, nil, nil, err } s.mu.Lock() // 检查该连接是否在Stmt.css里面,如果是,直接返回for _, v := range s.css { if v.dc == dc { s.mu.Unlock() return dc, dc.releaseConn, v.ds, nil } } s.mu.Unlock() // 否则需要先用该连接预编译sql模板withLock(dc, func() { ds, err = s.prepareOnConnLocked(ctx, dc) }) if err != nil { dc.releaseConn(err) return nil, nil, nil, err } return dc, dc.releaseConn, ds, nil
}
prepareOnConnLocked:预编译完成后,将连接加入stmt.css
func (s *Stmt) prepareOnConnLocked(ctx context.Context, dc *driverConn) (*driverStmt, error) { si, err := dc.prepareLocked(ctx, s.cg, s.query) if err != nil { return nil, err } cs := connStmt{dc, si} s.mu.Lock() // 预编译完成后,将当前连接加入stmt.csss.css = append(s.css, cs) s.mu.Unlock() return cs.ds, nil
}
事务结合预编译
如果同时有事务和预编译,那么在执行Exec/Query时,稍微有点不一样
回到事务的Begin方法:
func (db *DB) Begin(opts ...*sql.TxOptions) *DB { // ...switch beginner := tx.Statement.ConnPool.(type) { // ... // 预编译模式下: case ConnPoolBeginner: tx.Statement.ConnPool, err = beginner.BeginTx(tx.Statement.Context, opt) default: err = ErrInvalidTransaction } if err != nil { tx.AddError(err) } return tx
}
如果是预编译模式,进入PreparedStmtDB.BeginTx,返回PreparedStmtTX实例
func (db *PreparedStmtDB) BeginTx(ctx context.Context, opt *sql.TxOptions) (ConnPool, error) { if beginner, ok := db.ConnPool.(TxBeginner); ok { tx, err := beginner.BeginTx(ctx, opt) return &PreparedStmtTX{PreparedStmtDB: db, Tx: tx}, err } // ...
}
接下来看看基于PreparedStmtTX执行增删改查有何特别的地方
这里以PreparedStmtTX.ExecContext为例:
- 调
PreparedStmtDB.prepare从缓存拿或新建一个Stmt - 调
sql.Tx.StmtContext先处理步骤1返回的Stmt,再执行Exec,重点在这里
func (tx *PreparedStmtTX) ExecContext(ctx context.Context, query string, args ...interface{}) (result sql.Result, err error) { stmt, err := tx.PreparedStmtDB.prepare(ctx, tx.Tx, true, query) if err == nil { result, err = tx.Tx.StmtContext(ctx, stmt.Stmt).ExecContext(ctx, args...) if errors.Is(err, driver.ErrBadConn) { tx.PreparedStmtDB.Mux.Lock() defer tx.PreparedStmtDB.Mux.Unlock() go stmt.Close() delete(tx.PreparedStmtDB.Stmts, query) } } return result, err
}
sql.Tx.StmtContext方法
- 拿到和事务tx绑定的连接dc
- 看dc是否预编译过该stmt,如果没有执行预编译操作
- 将事务tx放到
Stmt.cg字段,后面执行Exec获取连接时,优先从该字段获取- 也就是保证执行整个事务都要用同一个连接
func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt { dc, release, err := tx.grabConn(ctx) if err != nil { return &Stmt{stickyErr: err} } defer release(nil) if tx.db != stmt.db { return &Stmt{stickyErr: errors.New("sql: Tx.Stmt: statement from different database used")} } var si driver.Stmt var parentStmt *Stmt stmt.mu.Lock() if stmt.closed || stmt.cg != nil { // ...} else { stmt.removeClosedStmtLocked() // See if the statement has already been prepared on this connection, // and reuse it if possible. for _, v := range stmt.css { if v.dc == dc { si = v.ds.si break } } stmt.mu.Unlock() // 没预编译过,执行预编译if si == nil { var ds *driverStmt withLock(dc, func() { ds, err = stmt.prepareOnConnLocked(ctx, dc) }) if err != nil { return &Stmt{stickyErr: err} } si = ds.si } parentStmt = stmt } txs := &Stmt{ db: tx.db, // 重点在这,将tx放到Stmt.cg中。后面执行Exec获取连接时,优先从该字段获取cg: tx, cgds: &driverStmt{ Locker: dc, si: si, }, parentStmt: parentStmt, query: stmt.query, } if parentStmt != nil { tx.db.addDep(parentStmt, txs) } tx.stmts.Lock() tx.stmts.v = append(tx.stmts.v, txs) tx.stmts.Unlock() return txs
}
总结
至此,gorm的源码分析告一段落了,下一篇文章会介绍一些工程上使用gorm的最佳实践
相关文章:
gorm源码解析(四):事务,预编译
文章目录 前言事务自己控制事务用 Transaction方法包装事务 预编译事务结合预编译总结 前言 前几篇文章介绍gorm的整体设计,增删改查的具体实现流程。本文将聚焦与事务和预编译部分 事务 自己控制事务 用gorm框架,可以自己控制事务的Begin࿰…...
前端优雅(装逼)写法(updating····)
1.>>右位移运算符取整数 它将一个数字的二进制位向右移动指定的位数,并在左侧填充符号位(即负数用1填充,正数用0填充)。 比如 2.99934 >> 0:取整结果是2,此处取整并非四舍五入 2.99934 会先…...
黑马Java面试教程_P7_常见集合_P4_HashMap
系列博客目录 文章目录 系列博客目录4. HashMap相关面试题4.4 面试题-HashMap的put方法的具体流程 频54.4.1 hashMap常见属性4.4.2 源码分析 HashMap的构造函数面试文稿: 4.5 讲一讲HashMap的扩容机制 难3频4面试文稿: 4.6 面试题-hashMap的寻址算法 难4…...
使用 CFD 加强水资源管理:全面概述
探索 CFD(计算流体动力学)在增强保护人类健康的土木和水利工程实践方面的重大贡献。 挑战 水资源管理是指规划、开发、分配和管理水资源最佳利用的做法。它包括广泛的活动,旨在确保水得到有效和可持续的利用,以满足各种需求&…...
XXE练习
pikachu-XXE靶场 1.POC:攻击测试 <?xml version"1.0"?> <!DOCTYPE foo [ <!ENTITY xxe "a">]> <foo>&xxe;</foo> 2.EXP:查看文件 <?xml version"1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SY…...
R语言读取hallmarks的gmt文档的不同姿势整理
不同格式各有所用 1.读取数据框格式的 hallmarks <- clusterProfiler::read.gmt("~/genelist/h.all.v7.4.symbols.gmt") #返回的是表格 hallmarks$term<- gsub(HALLMARK_,"",hallmarks$term)适配Y叔的clusterProfiler的后续分析,比如整理后geneli…...
【Nginx-4】Nginx负载均衡策略详解
在现代Web应用中,随着用户访问量的增加,单台服务器往往难以承受巨大的流量压力。为了解决这一问题,负载均衡技术应运而生。Nginx作为一款高性能的Web服务器和反向代理服务器,提供了多种负载均衡策略,能够有效地将请求分…...
Python 的 Decimal的错误计算
摘要 阐述在使用 Python的 Decimal类时,可能产生的错误计算。 在 详述 BigDecimal 的错误计算 中,笔者较为详细地说明了 Java的 BigDecimal可能出错的原因。类似地,Python的 decimal模块中有个 Decimal类,也可用于高精度的十进制…...
【韩顺平 Java满汉楼项目 控制台版】MySQL+JDBC+druid
文章目录 功能界面用户登录界面显示餐桌状态预定显示所有菜品点餐查看账单结账退出满汉楼 程序框架图项目依赖项目结构方法调用图功能实现登录显示餐桌状态订座显示所有菜品点餐查看账单结账退出满汉楼 扩展思考多表查询如果将来字段越来越多怎么办? 员工信息字段可…...
【HAL库】STM32CubeMX开发----STM32F407----Time定时器中断实验
STM32CubeMX 下载和安装 详细教程 【HAL库】STM32CubeMX开发----STM32F407----目录 前言 本次实验以 STM32F407VET6 芯片为MCU,使用 25MHz 外部时钟源。 实现定时器TIM3中断,每1s进一次中断。 定时器计算公式如下: arr 是自动装载值&#x…...
react18+ts 封装图表及词云组件
react18ts 封装图表及词云组件 1.下载依赖包 "echarts": "^5.5.1","echarts-for-react": "^3.0.2","echarts-wordcloud": "^2.1.0",2.创建目录结构 3.代码封装 ChartCard.tsx Wordcloud.tsx 4.调用 import Rea…...
图像根据mask拼接时,边缘有色差 解决
目录 渐变融合(Feathering) 沿着轮廓线模糊: 代码: 泊松融合 效果比较好: 效果图: 源代码: 泊松融合,mask不扩大试验 效果图: 源代码: 两个图像根据mask拼接时,边缘有色差 渐变融合(Feathering) import numpy as np import cv2# 假设 img1, img2 是两个…...
17、ConvMixer模型原理及其PyTorch逐行实现
文章目录 1. 重点2. 思维导图 1. 重点 patch embedding : 将图形分割成不重叠的块作为图片样本特征depth wise point wise new conv2d : 将传统的卷积转换成通道隔离卷积和像素空间隔离两个部分,在保证精度下降不多的情况下大大减少参数量 2. 思维导图 后续再整…...
Spring整合Redis基本操作步骤
Spring 整合 Redis 操作步骤总结 1. 添加依赖 首先,在 pom.xml 文件中添加必要的 Maven 依赖。Redis 相关的依赖包括 Spring Boot 的 Redis 启动器和 fastjson(如果需要使用 Fastjson 作为序列化工具): <!-- Spring Boot Re…...
STM32使用SFUD库驱动W25Q64
SFUD简介 SFUD是一个通用SPI Flash驱动库,通过SFUD可以库轻松完成对SPI Flash的读/擦/写的基本操作,而不用自己去看手册,写代码造轮子。但是SFUD的功能不仅仅于此:①通过SFUD库可以实现在一个项目中对多个Flash的同时驱动&#x…...
ArKTS基础组件
一.AlphabetIndexer 可以与容器组件联动用于按逻辑结构快速定位容器显示区域的组件。 子组件 color:设置文字颜色。 参数名类型必填说明valueResourceColor是 文字颜色。 默认值:0x99182431。 selectedColor:设置选中项文字颜色。 参数名类型必填说明valueRes…...
如何理解TCP/IP协议?如何理解TCP/IP协议是什么?
理解TCP/IP协议 1. 什么是TCP/IP协议? TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是一组用于实现网络通信的协议,广泛用于互联网和局域网中。TCP/IP协议栈由一系列协议组成,规定了计算机如何在网络中发送和接收数据。它通常被用来…...
如何使用 Python 连接 SQLite 数据库?
SQLite是一种轻量级的嵌入式数据库,广泛应用于各种应用程序中。 Python提供了内置的sqlite3模块,使得连接和操作SQLite数据库变得非常简单。 下面我将详细介绍如何使用sqlite3模块来连接SQLite数据库,并提供一些实际开发中的建议和注意事项…...
【博弈模型】古诺模型、stackelberg博弈模型、伯特兰德模型、价格领导模型
博弈模型 1、古诺模型(cournot)(1)假设(2)行为分析(3)经济后果(4)例题 2、stackelberg博弈模型(产量领导模型)(1ÿ…...
单片机:实现花样灯数码管的显示(附带源码)
单片机实现花样灯数码管显示 数码管(七段数码管)广泛用于数字显示,例如时钟、计数器、温度计等设备。在本项目中,我们将使用单片机实现花样灯数码管的显示效果。所谓花样灯显示是指通过控制数码管上的各个段位,以不同…...
RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...
相机从app启动流程
一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...
VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...
DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
抽象类和接口(全)
一、抽象类 1.概念:如果⼀个类中没有包含⾜够的信息来描绘⼀个具体的对象,这样的类就是抽象类。 像是没有实际⼯作的⽅法,我们可以把它设计成⼀个抽象⽅法,包含抽象⽅法的类我们称为抽象类。 2.语法 在Java中,⼀个类如果被 abs…...
springboot 日志类切面,接口成功记录日志,失败不记录
springboot 日志类切面,接口成功记录日志,失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...
Selenium 查找页面元素的方式
Selenium 查找页面元素的方式 Selenium 提供了多种方法来查找网页中的元素,以下是主要的定位方式: 基本定位方式 通过ID定位 driver.find_element(By.ID, "element_id")通过Name定位 driver.find_element(By.NAME, "element_name"…...
