Java Web|day5.MyBatis

MyBatis

定义

它是一款半自动的ORM持久层框架,具有较高的SQL灵活性,支持高级映射(一对一,一对多),动态SQL,延迟加载和缓存等特性,但它的数据库无关性较低

**ORM: **Object Relation Mapping,对象关系映射。对象指的是Java对象,关系指的是数据库中的关系模型,对象关系映射,指的就是在Java对象和数据库的关系模型之间建立一种对应关系,比如用一个Java的Student类,去对应数据库中的一张student表,类中的属性和表中的列一一对应。Student类就对应student表,一个Student对象就对应student表中的一行数据

半自动: 用mybatis进行开发,需要手动编写SQL语句。而全自动的ORM框架,如hibernate,则不需要编写SQL语句。

优点

  1. 灵活性高,自由地对sql进行定制

  2. 但相比JDBC,它提供了输入映射和输出映射,可以很方便地进行SQL参数设置,以及结果集封装。

  3. 还提供了关联查询和动态SQL等功能,极大地提升了开发的效率。

缺点

当要切换数据库时,SQL语句可能就要重写,因为不同的数据库有不同的方言(Dialect),所以mybatis的数据库无关性低。

占位符

#{}

  • #{}在mybatis中,最后会被解析为?,其实就是Jdbc的PreparedStatement中的?占位符,它有预编译的过程
  • 预编译
    • 它有预编译的过程,会对输入参数进行类型解析(如果入参是String类型,设置参数时会自动加上引号),可以防止SQL注入
  • 注意
    • 如果parameterType属性指定的入参类型是简单类型的话(简单类型指的是8种java原始类型再加一个String),#{}中的变量名可以任意
    • 如果入参类型是pojo,比如是Student类,那么#{name}表示取入参对象Student中的name属性,#{age}表示取age属性

${}

  • ${},一般会用在模糊查询的情景,比如SELECT * FROM student WHERE name like '%${name}%';
  • 处理阶段在#{}之前,它不会做参数类型解析,而仅仅是做了字符串的拼接,
  • 入参的Student对象的name属性为zhangsan,则上面那条SQL最终被解析为SELECT * FROM student WHERE name like ‘%zhangsan%’;
  • 模糊查询只能用${}
  • 对于简单类型(8种java原始类型再加一个String)的入参,${}中参数的名字必须是value

使用过程

原生流程

  1. 编写mapper.xml,书写SQL,并定义好SQL的输入参数,和输出参数
  2. 编写全局配置文件,配置数据源,以及要加载的mapper.xml文件
  3. 通过全局配置文件,创建SqlSessionFactory
  4. 每次进行CRUD时,通过SqlSessionFactory创建一个SqlSession
  5. 调用SqlSession上的selectOne,selectList,insert,delete,update等方法,传入mapper.xml中SQL标签的id,以及输入参数
  6. 提交sqlSession.commit()
  7. 关闭资源sqlSession.close()

基于Mapper代理

为了简化开发,mybatis提供了mapper接口代理的开发方式,不需要再编写dao类,只需要编写一个mapper接口,一个mapper的接口和一个mapper.xml相对应,只需要调用SqlSession对象上的getMapper(),传入mapper接口的class信息,即可获得一个mapper代理对象,直接调用mapper接口中的方法,即相当于调用mapper.xml中的各个SQL标签,此时就不需要指定SQL标签的id字符串了,mapper接口中的一个方法,就对应了mapper.xml中的一个SQL标签

注意
  • mapper接口的全限定名,要和mapper.xml的namespace属性一致
  • mapper接口中的方法名要和mapper.xml中的SQL标签的id一致
  • mapper接口中的方法入参类型,要和mapper.xml中SQL语句的入参类型一致
  • mapper接口中的方法出参类型,要和mapper.xml中SQL语句的返回值类型一致
使用方法
  1. 全局配置文件
<!-- 普通加载xml --> 
<mappers>    <mapper resource="StudentMapper.xml"/> 
</mappers>
  1. mapper.java
    在这里插入图片描述

  2. mapper.xml
    在这里插入图片描述

  3. 测试调用

请添加图片描述

基于注解的开发

还是得有一个全局配置的xml文件,不过mapper.xml就可以省掉了

注意

当使用注解开发时,若需要传入多个参数,可以结合@Param注解

  • @Param标签会被mybatis处理并封装成一个Map对象,比如上面的示例中,实际传入的参数是一个Map对象,@Param标签帮忙向Map中设置了值,即它做了

  • Map<String,Object> map = new HashMap<>(); map.put("name", name); 
    map.put("major",major);
    

步骤

  1. 创建一个Mapper接口
<!-- 在mapper接口中使用注解 --> 
<mappers>    <mapper class="com.yogurt.mapper.PureStudentMapper"/> 
</mappers>
  1. 在全局配置文件中修改标签,直接指定加载这个类

三种加载Mapper的方式

  1. <!-- 普通加载xml -->
    <mappers><mapper resource="StudentMapper.xml"/>
    </mappers>
    
  2. <!-- 在mapper接口中使用注解 -->
    <mappers><mapper class="com.yogurt.mapper.PureStudentMapper"/>
    </mappers>
    
  3. <!-- 使用<package>标签 -->
    <mappers><package name="com.yogurt.mapper"/>
    </mappers>
    
  1. 自动加载com.yogurt.mapper包下的所有mapper
  2. 这种方式需要将mapper接口文件和mapper.xml文件都放在com.yogurt.mapper包下,且接口文件和xml文件的文件名要一致。
  3. 若mapper接口采用注解方式,则不需要xml;若mapper接口没有采用注解方式,则mapper接口和xml文件的名称要相同,且在同一目录

注意

是因为,对于src/main/java 源码目录下的文件,maven打包时只会将该目录下的java文件打包,而其他类型的文件都不会被打包进去,去工程目录的target目录下看看maven构建后生成的文件

解决

需要在pom.xml中的 标签下 添加 标签,指定打包时要将xml文件打包进去

<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes></resource></resources>
</build>

运行流程

流程

  1. 利用Xpath语法解析全局配置文件
  2. 解析全局配置,
    • 是否开启二级缓存
    • 是否开启延迟加载
    • 是否使用插件(plugin)
    • 是否设置类型别名(typeAlias)
    • 数据库连接信息
  3. 解析mapper映射文件,将每个CRUD标签,封装为一个MappedStatement
  4. 所有的信息封装到Configuration对象(这是一个重量级对象)
  5. 将Configuration封装到SqlSessionFactory中
  6. 每次调用SqlSessionFactory开启一个SqlSession
  7. 每个SqlSession,会持有一个私有的Executor对象,以及共享的Configuration对象
  8. 每次操作,选取一个MappedStatement,由Executor执行。包括SQL语句组装,参数解析,返回结果解析等操作

图示

请添加图片描述

关键类

  1. Configuration

    封装了所有的属性

  2. MappedStatement

    一个CRUD标签,对应一个MappedStatement

  3. Executor体系

    mybatis核心执行器,通过Executor来执行数据库操作

  4. SqlSource体系

    封装CRUD标签的一个SqlSource,用于组装SQL语句

  5. SqlNode体系

    以树状形式,存储动态SQL标签,一个SqlSource拥有一个rootSqlNode,每个SqlNode有一个apply方法,在Executor执行时用于拼接SQL语句,SqlNode的设计用到的是设计模式中的组合模式。

  6. StatementHandler

    生成Statement,设置参数,执行查询

  7. ParameterHandler

    设置参数时,进行参数解析,类型转换等

  8. ResultSetHandler

    获取结果集,并进行结果封装

MyBatis缓存

定义

mybatis缓存,将数据库的查询结果,保存到内存(或者硬盘),以便下次执行相同查询时,不经过数据库,直接从内存中取出结果

作用

对于重复的查询,它能够提高响应速度,并减轻数据库的访问压力。适用于对响应时间要求高,而数据实时性要求不高的情况下。

分类

一级缓存

作用范围

  • SqlSession(默认)

持有者

  • BaseExecutor

默认开启

  • 其实是无法关闭的,但可以通过一些方法来使一级缓存失效

清除缓存

  • 在同一个SqlSession里执行 增 删 改 操作时(不必提交),会清除一级缓存
  • SqlSession提交或关闭时(关闭时会自动提交),会清除一级缓存
  • 对mapper.xml中的某个CRUD标签设置属性flushCache=true (这样会导致该标签的一级缓存和二级缓存都失效)
  • 在全局配置文件中设置 ,这样会使一级缓存失效,二级缓存不受影响
二级缓存

作用范围

  • mapper级别(可以跨SqlSession),一个mapper.xml即对应一个二级缓存,每个二级缓存,以mapper文件的namespace作为唯一标识

持有者

  • MappedStatement

默认关闭

  • (其实全局配置文件中的cacheEnabled默认是true,这个是二级缓存总开关,默认是已经打开了的,然而必须要在mapper.xml中配置 标签,才能开启该mapper.xml的二级缓存)

开启效果

  • 映射语句文件中的所有SELECT 语句将会被缓存。
  • 映射语句文件中的所有时INSERT 、UPDATE 、DELETE 语句会刷新缓存。
  • 缓存会使用Least Rece ntly U sed ( LRU ,最近最少使用的)算法来收回。
  • 根据时间表(如no Flush Int erv al ,没有刷新间隔),缓存不会以任何时间顺序来刷新。
  • 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024 个引用。
  • 缓存会被视为read/write(可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

注意

  • 对于SELECT节点对应的MappedStatement,默认是开启二级缓存,对其他节点(INSERT/DELETE/UPDATE),默认是关闭(这是由MappedStatement里的useCache属性控制的)
  • 放入二级缓存的数据,默认要实现Serializable接口,因为二级缓存的存储介质除了内存,还可能存在硬盘,所以存储的对象需要可序列化。
    • readOnly=false:通过序列化,返回缓存对象的一份拷贝,速度上会慢一些,但是更加安全。用户取得数据后,执行修改,并不会影响到二级缓存中的对象
    • 这些对象可以随意修改,不会影响后续相同查询条件的结果。这会慢一些,但是安全,因此默认是false。
  • 当然,若把二级缓存的readOnly属性设为true,则对象数据可以不用实现Serializable接口
    • readOnly=true:返回缓存对象的引用。
    • 因此返回的这些缓存对象自己没事不要去修改。一旦修改,就会影响下一个相同查询条件的查询结果。这提供了很重要的性能优势。
    • readOnly=true意在告诉用户,从缓存中取出数据后,不要对数据进行修改,而不是保证缓存的只读性(cache中存的是对象引用,取出数据后若执行修改,则会改变真正的对象,另外的用户再从cache中取对象,则会发现对象已经被修改)
  • 执行一次查询后,需要进行提交,该次查询结果才会保存到二级缓存中

存在问题

  • 由于是每个namespace对应一个二级缓存(一个namespace就是一个mapper.xml)
  • 若mapperA.xml中全是对user表的操作,而在另一个mapperB.xml中也有少许对user表的操作,这2个mapper的二级缓存是互相独立的,然而mapperB若对user表执行了增删改,并提交,却不会刷新到mapperA的二级缓存,此时用mapperA去做查询,则可能取到脏数据。

应用场景

主键返回

在插入一条记录时,我们不设置其主键id,而让数据库自动生成该条记录的主键id,那么在插入一条记录后,如何得到数据库自动生成的这条记录的主键id呢

两种方式

  1. 使用useGeneratedKeys和keyProperty属性
<insert id="insert" parameterType="com.yogurt.po.Student" useGeneratedKeys="true" keyProperty="id">INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});
</insert>
  1. 使用子标签
<insert id="insert" parameterType="com.yogurt.po.Student">INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});<selectKey keyProperty="id" order="AFTER" resultType="int" >SELECT LAST_INSERT_ID();</selectKey>
</insert>

标签其实就是一条SQL,这条SQL的执行,可以放在主SQL执行之前或之后,并且会将其执行得到的结果封装到入参的Java对象的指定属性上。

注意子标签只能用在和标签中。上面的LAST_INSERT_ID()实际上是MySQL提供的一个函数,可以用来获取最近插入或更新的记录的主键id。

批量查询

主要是动态SQL标签的使用,注意如果parameterType是List的话,则在标签体内引用这个List,只能用变量名list,如果parameterType是数组,则只能用变量名array

mapper定义

<select id="batchFind" resultType="student" parameterType="java.util.List">SELECT * FROM student<where><if test="list != null and list.size() > 0">AND id in<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach></if></where>
</select>

传参

List<Student> students = mapper.batchFind(Arrays.asList(1, 2, 3, 7, 9));

动态sql

可以根据具体的参数条件,来对SQL语句进行动态拼接。

注意

由于不确定查询参数是否存在,许多人会使用类似于where 1 = 1 来作为前缀,然后后面用AND 拼接要查询的参数,这样,就算要查询的参数为空,也能够正确执行查询,如果不加1 = 1,则如果查询参数为空,SQL语句就会变成SELECT * FROM student where ,SQL不合法。

  1. if
<select id="find" resultType="student" parameterType="student">SELECT * FROM student WHERE age >= 18<if test="name != null and name != ''">AND name like '%${name}%'</if>
</select>
  1. choose
<select id="findActiveBlogLike"resultType="Blog">SELECT * FROM BLOG WHERE state = ‘ACTIVE’<choose><when test="title != null">AND title like #{title}</when><when test="author != null and author.name != null">AND author_name like #{author.name}</when><otherwise>AND featured = 1</otherwise></choose>
</select> 
  1. where

    标签只会在至少有一个子元素返回了SQL语句时,才会向SQL语句中添加WHERE,并且如果WHERE之后是以AND或OR开头,会自动将其删掉

<select id="findActiveBlogLike"resultType="Blog">SELECT * FROM BLOG<where><if test="state != null">state = #{state}</if><if test="title != null">AND title like #{title}</if><if test="author != null and author.name != null">AND author_name like #{author.name}</if></where>
</select>
  1. trim

    标签可以用标签代替

<trim prefix="WHERE" prefixOverrides="AND | OR">...
</trim>
  1. foreach

    用来做迭代拼接的,通常会与SQL语句中的IN查询条件结合使用,注意,到parameterType为List(链表)或者Array(数组),后面在引用时,参数名必须为list或者array。如在foreach标签中,collection属性则为需要迭代的集合,由于入参是个List,所以参数名必须为list

<select id="batchFind" resultType="student" parameterType="list">SELECT * FROM student WHERE id in<foreach collection="list" item="item" open="(" separator="," close=")">#{item}</foreach>
</select>
  1. sql

    可将重复的SQL片段提取出来,然后在需要的地方,使用标签进行引用

<select id="findUser" parameterType="user" resultType="user">SELECT * FROM user<include refid="whereClause"/>
</select><sql id="whereClause"><where><if test="user != null">AND username like '%${user.name}%'</if></where>
</sql>
  1. bind

    mybatis的动态SQL都是用OGNL表达式进行解析的,如果需要创建OGNL表达式以外的变量,可以用bind标签

<select id="selectBlogsLike" resultType="Blog"><bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />SELECT * FROM BLOGWHERE title LIKE #{pattern}
</select>

关联查询

使用 标签以及和 子标签,进行关联查询,

延迟加载

定义

在关联查询时,利用延迟加载,先加载主信息。需要关联信息时再去按需加载关联信息

优点

这样会大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。

用法

在mybatis中,resultMap标签 的association标签和collection标签具有延迟加载的功能。

原理

开启前

  • 第一种方法:我们直接关联查询出所有订单和用户的信息
  • select * from orders o ,user u where o.user_id = u.id;

开启后

  • 第二种方法:分步查询,首先查询出所有的订单信息,然后如果需要用户的信息,我们在根据查询的订单信息去关联用户信息
  • select * from orders;
  • select * from user where id=user_id

逆向工程

mybatis官方提供了mapper自动生成工具mybatis-generator-core来针对单表,生成PO类,以及Mapper接口和mapper.xml映射文件。针对单表,可以不需要再手动编写xml配置文件和mapper接口文件了,非常方便。美中不足的是它不支持生成关联查询。一般做关联查询,就自己单独写SQL就好了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/402248.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字孪生技术框架:从数据到决策的桥梁

随着科技的飞速发展&#xff0c;数字孪生技术作为一种创新的信息化手段&#xff0c;正逐步渗透到各个行业领域&#xff0c;成为推动数字化转型的重要力量。数字孪生技术框架&#xff0c;作为支撑这一技术体系的核心架构&#xff0c;以其独特的层级结构&#xff0c;实现了从数据…

如何选择适合流水线作业的工业级条码读码器

高度自动化的工业生产环境中&#xff0c;条码二维码的应用&#xff0c;在工厂流水线作业的高效性和准确性对于企业的生产效率和质量管控至关重要&#xff0c;条码二维码扫描设备作为流水线中用于读取条码或二维码朔源信息的关键设备&#xff0c;它们能够快速、准确地识别和采集…

vim - vim模式及部分操作

文章目录 一、vim 基本介绍二、vim 的简单使用三、几种常用模式切换四、命令模式和底行模式的操作汇总 一、vim 基本介绍 vim 是一款多模式的编辑器。vim 中有很多子命令来进行代码的编写操作。 同时&#xff0c;vim 提供了不同的模式供我们选择。 在vim下的底行模式下通过:he…

8.15 day bug

bug1 一个按钮折腾了 两个小时 一直点第一个按钮&#xff0c;然后进去后发现根本没有课程&#xff0c;需要创建workspace&#xff0c;然后各种问题&#xff0c;还是没把课程启动起来&#xff0c;然后去看gitpod使用文档&#xff0c;搞懂工作区到底是怎么回事&#xff0c;一通操…

JVM 有哪些垃圾回收算法(回收机制)?

JVM 有哪些垃圾回收算法&#xff08;回收机制&#xff09;&#xff1f; 标记-清除算法 在Java虚拟机中&#xff0c;标记-清除算法是一种用于垃圾回收的算法。它分为两个阶段&#xff1a;标记阶段和清除阶段。 在标记阶段&#xff0c;垃圾收集器会遍历堆内存中的所有对象&…

JavaEE 第11节 定时器

前言 本篇博客重点介绍定时器的简单实现&#xff0c;帮助理解其底层原理。关于JAVA工具类自带的定时器&#xff0c;只会简单介绍&#xff0c;详细使用参阅官方文档&#xff08;下文中有官方文档的连接&#xff09;。 一、什么是定时器 定时器的概念非常简单。 它在软件开发…

自制深度学习推理框架之计算图设计

文章目录 一、计算图1.1 计算图定义1.2 计算图的生成1.2.1 **静态计算图&#xff08;Static Computational Graph&#xff09;**1.2.2 **动态计算图&#xff08;Dynamic Computational Graph&#xff09;** 1.3 计算图功能1.3.1 训练阶段1.3.2 推理部署阶段 1.4 计算图的调度(执…

引领企业全球化发展 极光亮相华为亚太ICT峰会2024·泰国

近日&#xff0c;华为亚太ICT峰会2024泰国正式开幕&#xff0c;极光&#xff08;Aurora Mobile&#xff0c;纳斯达克股票代码&#xff1a;JG&#xff09;凭借其创新的技术实力与前瞻性的产品布局&#xff0c;受邀出席本次活动。会上&#xff0c;极光展示了其全域消息通知解决方…

【ocr识别003】flask+paddleocr+bootstrap搭建OCR文本推理WEB服务

1.欢迎点赞、关注、批评、指正&#xff0c;互三走起来&#xff0c;小手动起来&#xff01; 2.了解、学习OCR相关技术知识领域&#xff0c;结合日常的场景进行测试、总结。如本文总结的flaskpaddleocrbootstrap搭建OCR文本推理WEB服务应用示例场景。 文章目录 1.代码结构2.效果演…

MySQL 在 Windows 和 Ubuntu 上的安装与远程连接配置简介

MySQL 是一个广泛使用的开源关系型数据库管理系统&#xff0c;它提供了多用户、多线程的数据库服务。本文将介绍如何在 Windows 和 Ubuntu 操作系统上安装 MySQL&#xff0c;并配置远程连接。 Windows 上的 MySQL 安装 1. 下载 MySQL Installer 访问 MySQL 官方网站下载 Win…

融合创新:EasyCVR视频汇聚平台云计算技术与AI技术共筑雪亮工程智能防线

随着信息技术的飞速发展&#xff0c;视频云计算技术作为云计算领域的一个重要分支&#xff0c;正逐步在公共安全、社会治理等领域展现出其独特的优势。特别是在雪亮工程这一群众性治安防控工程中&#xff0c;视频云计算技术更是发挥了不可替代的作用。本文将从视频云计算技术的…

【leetcode详解】特殊数组II : 一题代表了一类问题(前缀和思想)

前缀和的优势 给定一个数组&#xff0c;前缀和的特点在于&#xff0c;任意给出一对始末位置&#xff0c;能够用O(1)的时间复杂度得到始末位置之间所有元素的某种关系。 题型分析 这道题目正是“给出始末位置&#xff0c;检测其中元素特点”那一类&#xff0c;那我们就想&#…

自动化与高效设计:推理技术在FPGA中的应用

想象一下&#xff0c;你正在设计一个复杂的电路系统&#xff0c;就像在搭建一座精巧的积木城堡。你手头有各种形状和功能的积木块&#xff0c;这些积木块可以组合成任何你需要的结构。在这个过程中&#xff0c;你有两种主要的方法&#xff1a;一种是手动挑选和搭建每一块积木&a…

【Qt】内置对话框

一.Qt内置对话框 Qt 提供了多种可复⽤的对话框类型&#xff0c;即 Qt 标准对话框。Qt标准对话框全部继承于QDialog类。常⽤标准对话框如下&#xff1a; 二.内置对话框分类 1.消息对话框 QMessageBox 1.1 概念 消息对话框是应⽤程序中最常⽤的界⾯元素。消息对话框主要⽤于为…

Android-RK356x GT9XX多点触控设置为单点触控的方法

本文基于RK356x Android11系统描述GT9XX驱动芯片由多点触摸改为单点触摸功能。本次介绍的是触觉智能的Purple Pi OH鸿蒙开源主板&#xff0c;Purple Pi OH是华为Laval官方社区主荐的一款鸿蒙开发主板。 该主板主要针对学生党&#xff0c;极客&#xff0c;工程师&#xff0c;极大…

Opencv模板匹配

使用OpenCV和C来识别彩色图片中的特定物体&#xff0c;如黑桃♠&#xff0c;通常涉及几个步骤&#xff1a;预处理图像、特征提取、对象检测等。下面是一个基本的示例代码&#xff0c;演示如何使用OpenCV的模板匹配方法来识别图片中的黑桃♠。 函数原型 void matchTemplate(Inp…

【Mac】植物大战僵尸杂交版 for Mac(经典策略塔防游戏)游戏介绍

游戏介绍 植物大战僵尸杂交版 for Mac是一款非常受欢迎的策略塔防游戏&#xff0c;植物大战僵尸游戏以其独特的主题、幽默的风格和富有挑战性的关卡设计而著称。玩家需要种植各种植物来防御入侵的僵尸&#xff0c;每种植物都有其特定的功能和攻击方式。植物大战僵尸杂交版&…

老友记台词 第一季 第十五集 Friends 115(全英版)

文章目录 115 The One With the Stoned Guy[Scene: Central Perk, Rachel is serving Joey, Ross, and Monica their drinks.][Scene: Chandlers job, Chandler is typing data into his computer, he keeps typing even while taking a drink of coffee with one hand. One of…

VScode前端环境搭建

前言 VScode是企业中最常用的前端开发工具&#xff0c;本文描述如何利用VScode搭建前端开发环境 一、安装VScode 下载Vscode 点击前往下载页面 安装 安装时一直点击下一步即可 二、环境配置 1&#xff09;更改语言 点击拓展搜索Chinese后下载第一个&#xff0c;下载完后…

Bruno API 工具

Bruno 是Postman 和Insomnia 的开源桌面替代品&#xff0c;用于 API 的测试、开发和调试。它将测试集合保存在本地&#xff0c;因此可以使用 Git 或其他版本控制工具来进行协作。 下载地址: https://www.usebruno.com/downloads 功能 1. 左边菜单 Collections Create Collec…