当前位置: 首页 > news >正文

Android - 分区存储 MediaStore、SAF

官方页面
参考文章

一、概念

        分区存储(Scoped Storage)的推出是针对 APP 访问外部存储的行为(乱建乱获取文件和文件夹)进行规范和限制,以减少混乱使得用户能更好的控制自己的文件。

        公有目录被分为两大类:媒体文件(图片、音频、视频)的访问使用 MediaStore,其它文件通过系统的文件选择器访问 Storage Access Framework(简称SAF)。

二、MediaStore

跳转ContentProvider

class MediaStore.Images所有图片内容的类。
class MediaStore.Video所有视频内容的类。
class MediaStore.Audio所有音频内容的类。
class MediaStore.Files文件储存库中所有文件的索引,包括非媒体文件和媒体文件类。
interface MediaStore.MediaColumns文件储存库中表的公共字段(文件的各种信息)。

2.1 获取 Uri

使用 Context 获取到 ContentResolver 对象,通过 Uri 即可获取各种媒体库的 ContentProvider,从而对媒体文件进行操作。 

文件类型MediaStore 常量Uri 地址
图片MediaStore.Images.Media.EXTERNAL_CONTENT_URIcontent://media/external/images/media
视频MediaStore.Video.Media.EXTERNAL_CONTENT_URIcontent://media/external/video/media
音频MediaStore.Audio.Media.EXTERNAL_CONTENT_URIcontent://media/external/audio/media
非媒体文件MediaStore.Downloads.Media.EXTERNAL_CONTENT_URIcontent://media/external/downloads
val uri1 = Uri.parse("content://media/external/images/media")
val uri2 = MediaStore.Images.Media.getContentUri("external")
val uri3 = MediaStore.Images.Media.EXTERNAL_CONTENT_URI    //推荐

2.2 读取媒体文件

列名(文件信息)可以在 MediaStore.MediaColumns 取公共常量字段,也可以根据文件类型的不同在具体内部类中取值。

文件类型MediaStore 常量(常用列名)说明
图片

MediaStore.Images.Media._ID

磁盘上文件的路径

MediaStore.Images.Media.DATA

磁盘上文件的路径

MediaStore.Images.Media.DATE_ADDED

文件添加到media provider的时间(单位秒)

MediaStore.Images.Media.DATE_MODIFIED

文件最后一次修改单元的时间

MediaStore.Images.Media.DISPLAY_NAME

文件的显示名称

MediaStore.Images.Media.HEIGHT

图像/视频的高度,以像素为单位

MediaStore.Images.Media.MIME_TYPE

文件的 MIME 类型

MediaStore.Images.Media.SIZE

文件的字节大小

MediaStore.Images.Media.TITLE

标题

MediaStore.Images.Media.WIDTH

图像/视频的宽度,以像素为单位
视频

MediaStore.Video.Media.TITLE

名称

MediaStore.Video.Media.DURATION

总时长

MediaStore.Video.Media.DATA

地址

MediaStore.Video.Media.SIZE

大小

MediaStore.Video.Media.WIDTH

视频的宽度,以像素为单位

MediaStore.Video.Media.HEIGHT

视频的高度,以像素为单
音频

MediaStore.Audio.Media.TITLE

歌名

MediaStore.Audio.Media.ARTIST

歌手

MediaStore.Audio.Media.DURATION

总时长

MediaStore.Audio.Media.DATA

地址

MediaStore.Audio.Media.SIZ

大小

public final Cursor query (

    Uri uri,        //要查询的 ContentProvider 的 Uri

    String[] projection,        //要查询的字段(列Column),用 null 表示返回所有字段内容。

    String selection,        //查询条件,相当于SQL语句中的where,用 null 表示不进行筛选。

    String[] selectionArgs,        //如果 selection 里有?符号这里可以以实际值代替。没有的话可以为null。

    String sortOrder        //对结果进行排序,相当于SQL语句中的Order by,升序 asc /降序 desc,null为默认排序。

)

返回的是一个封装了结果集的游标对象 Cursor ,资源用完需要调用 close() 关闭。

//获取图片类型的Uri
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
//要获取的信息(列名)
val projection = arrayOf(MediaStore.Images.Media._ID,    //获取IDMediaStore.Images.Media.MIME_TYPE,  //获取MIME_TYPEMediaStore.Images.Media.DISPLAY_NAME    //获取DISPLAY_NAME
)
//筛选条件(png格式的图片)
val selection = "${MediaStore.Images.Media.DISPLAY_NAME}='.png'"  // ='xx.png' 改成 =?
//筛选条件的参数
val selectionArgs = arrayOf(".png")   //替换筛选条件语句中?部分
//对结果的排序方式
val sortOrder = "${ContactsContract.Contacts._ID} DESC" //注意:desc前有空格
//开始查询(返回的是一个封装了结果集的游标对象,资源用完需要关闭使用use函数)
contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)?.use { cursor ->//表都是通过行和列定位到具体的位置然后数据将其取出cursor.run {//获取字段在第几列(查询什么才能取出什么,否则空指针异常)val idIndex  = getColumnIndexOrThrow(MediaStore.Images.Media._ID)val mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE)val displayNameIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)//循环取出每一行对应字段的数据while (moveToNext()) {val id = getLong(idIndex)val mineType = getString(mimeTypeIndex)val displayName = getString(displayNameIndex)//合成图片的UriContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)//TODO...}}
}
//获取到的 Uri 可以通过 Glide 显示
Glide.with(context).load(uri).into(imageView)
//手动解析成图片的话
contentResolver.openFileDescriptor(uri, "")?.use {val bitmap = BitmapFactory.decodeFileDescriptor(it.fileDescriptor)imageView.setImageBitmap(bitmap)
}

2.3 写入媒体文件

通过 MediaStore 创建文件会保存到对应类型的默认目录中,也可以指定存放到其它同类型的公有目录或子文件夹中。如果存放到不同类型的公有目录中会报错 IllegalArgumentException(但是三种都可以存到Download中)。

文件类型mimeType 文件类型默认存储目录(其它允许存储目录)
图片image/*Pictures(DICM)
视频video/*Movies(DICM)
音频audio/*Music(Alarms、Notifications、Podcasts、Ringtones)
文件file/*Download

public final Uri insert( Uri url, ContentValues values) 

构造一个 ContentValues 对象通过 ContentResolver.insert 插入到对应的目录中,对返回的 Uri  对象进行文件流写入即可。

val values = ContentValues().apply {//指定 MimeTypeput(MediaStore.Images.Media.MIME_TYPE,"image/png")//指定文件名put(MediaStore.Images.Media.DISPLAY_NAME,"${System.currentTimeMillis()}.png")//指定保存的文件目录(如果不设置这个值,则会被默认保存到对应的媒体类型的文件夹下)if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {//Android 10中新增了一个RELATIVE_PATH常量,表示文件存储的相对路径,可选值有DIRECTORY_DCIM、DIRECTORY_PICTURES、DIRECTORY_MOVIES、DIRECTORY_MUSICput(MediaStore.Images.Media.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/DemoPicture")} else {//之前的系统版本中并没有RELATIVE_PATH,所以要使用 DATA 并拼装出一个文件存储的绝对路径才行put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}${File.separator}${Environment.DIRECTORY_DCIM}${File.separator}${System.currentTimeMillis()}.png")}
}
//插入文件数据库并获取到文件的Uri
val uri= contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
//对Uri进行文件流写入
uri?.let {//通过outputStream将本地图片bitmap或网络图片输入流写入UrlcontentResolver.openOutputStream(it)?.use { outputStream ->//TODO...//bitmap.compress(Bitmap.CompressFormat.PNG,100, outputStream)}
}

2.4 下载文件到Download目录

 方式和上面的写入一样,将网络获取的输入流写入。

  • 注意:MediaStore.Downloads是Android 10中新增的API,Android 9及以下的系统版本仍然使用之前的代码来进行文件下载。 
val inputStream = XXX.inputStream
val bis = BufferedInputStream(inputStream)
val buffer  = ByteArray(1024)//对Uri进行文件流写入
insertUri?.let {//通过outputStream将本地bitmap或网络输入流写入UrlcontentResolver.openOutputStream(it)?.use { outputStream ->BufferedOutputStream(outputStream).use { bos ->var bytes = bis.read(buffer)while (bytes >= 0) {bos.write(buffer, 0, bytes)bos.flush()bytes = bis.read(buffer)}}}
}

三、使用文件选择器 SAF

对于非媒体文件,无法像之前那样手写一个文件浏览器,而是必须使用系统提供的内置文件选择器。通过 Intent 启动系统的文件选择器,然后在 onActivityResult() 中获取到用户选中文件的 Uri 通过ContentResolver打开文件输入流来进行读取就可以了。

const val PICK_FILE = 1private fun pickFile() {val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)intent.addCategory(Intent.CATEGORY_OPENABLE)intent.type = "*/*"startActivityForResult(intent, PICK_FILE)
}override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)when (requestCode) {PICK_FILE -> {if (resultCode == Activity.RESULT_OK && data != null) {val uri = data.dataif (uri != null) {val inputStream = contentResolver.openInputStream(uri)// 执行文件读取操作}}}}
}

四、第三方库不支持的解决办法

编写一个文件复制功能,将Uri对象所对应的文件复制到应用程序的关联目录下,然后再将关联目录下这个文件的绝对路径传递给第三方SDK,这样就可以完美进行适配了。

fun copyUriToExternalFilesDir(uri: Uri, fileName: String) {val inputStream = contentResolver.openInputStream(uri)val tempDir = getExternalFilesDir("temp")if (inputStream != null && tempDir != null) {val file = File("$tempDir/$fileName")val fos = FileOutputStream(file)val bis = BufferedInputStream(inputStream)val bos = BufferedOutputStream(fos)val byteArray = ByteArray(1024)var bytes = bis.read(byteArray)while (bytes > 0) {bos.write(byteArray, 0, bytes)bos.flush()bytes = bis.read(byteArray)}bos.close()fos.close()}
}

五、管理设备上所有的文件(公有目录 + 自定义目录)

绝大部分的应用程序都不应该申请这个权限,仅适用于文件浏览器、病毒查杀类APP,需要跳转到系统页面让用户手动授权,Play商店上架也会更严格。即便得到授权也只能访问 公有目录 + 自定义目录,依然无法访问私有目录。

5.1 权限声明 

//不加 ignore 属性 AndroidStudio 会用警告提醒。
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"tools:ignore="ScopedStorage" />

5.2 跳转系统页面授权 

//系统低于11或者方法返回true说明已经拥有整个SD卡管理权限
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager()) {Toast.makeText(this, "已获得访问所有文件权限", Toast.LENGTH_SHORT).show()
} else {//否则弹窗告知申请原因并跳转到系统授权界面让用户手动授权val builder = AlertDialog.Builder(this).setMessage("本程序需要您同意允许访问所有文件权限").setPositiveButton("确定") { _, _ ->val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)startActivity(intent)}builder.show()
}

六、修改其它APP贡献的文件

修改其它APP贡献的文件是不安全的行为,默认情况下会抛异常,需要跳转到系统页面让用户手动授权,仅适用于美图秀秀类APP。在 Android 10 中每次跳转授权只能操作一张图片,如果一个程序需要修改很多张图片会很麻烦,在 Android 11 中提供了 Batch Operations 从而一次性对多个文件的操作权限进行申请。

  • 由于10 之前没有分区存储,10 和 11以后是两套处理方案,专门针对 10 一个版本去写处理方案会很麻烦,由于 10 不是强制启用分区存储,可以在 AndroidManifest 中配置 requestLegacyExternalStorage 来禁用。 

createWriteRequest()

请求对多个文件的写入权限。

createFavoriteRequest()

请求将多个文件加入到Favorite(收藏)的权限。

createTrashRequest()

请求将多个文件移至回收站的权限。

createDeleteRequest()

请求将多个文件删除的权限。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {/创建了一个集合用于存放所有要批量申请权限的文件Urival urisToModify = listOf(uri1, uri2, uri3, uri4)//创建一个PendingIntentval editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)//进行权限申请startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE, null, 0, 0, 0)
}override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)when (requestCode) {EDIT_REQUEST_CODE -> {if (resultCode == Activity.RESULT_OK) {Toast.makeText(this, "用户已授权", Toast.LENGTH_SHORT).show()} else {Toast.makeText(this, "用户没有授权", Toast.LENGTH_SHORT).show()}}}
}

相关文章:

Android - 分区存储 MediaStore、SAF

官方页面 参考文章 一、概念 分区存储&#xff08;Scoped Storage&#xff09;的推出是针对 APP 访问外部存储的行为&#xff08;乱建乱获取文件和文件夹&#xff09;进行规范和限制&#xff0c;以减少混乱使得用户能更好的控制自己的文件。 公有目录被分为两大类&#xff1a;…...

Shiro框架权限控制

首先去通过配置类的用户认证&#xff0c;在用户认证完成后&#xff0c;进行用户授权&#xff0c;用户通过授权之后再跳转其他的界面时&#xff0c;会进行一个验证&#xff0c;当前账号是否有权限。 前端权限控制显示的原理 在前端中&#xff0c;通常使用用户的角色或权限信息来…...

centOS7 安装tailscale并启用子网路由

1、在centOS7上安装Tailscale客户端 #安装命令所在官网位置&#xff1a;https://tailscale.com/download/linux #具体命令为&#xff1a; curl -fsSL https://tailscale.com/install.sh | sh #命令执行后如下图所示2、设置允许IP转发和IP伪装。 安装后&#xff0c;您可以启动…...

spring 项目中如何处理跨越cors问题

1.使用 CrossOrigin 注解 作用于controller 方法上 示例如下 RestController RequestMapping("/account") public class AccountController {CrossOriginGetMapping("/{id}")public Account retrieve(PathVariable Long id) {// ...}DeleteMapping(&quo…...

importlib --- import 的实现

3.1 新版功能. 源代码 Lib/importlib/__init__.py 概述 importlib 包具有三重目标。 一是在 Python 源代码中提供 import 语句的实现&#xff08;并且因此而扩展 __import__() 函数&#xff09;。 这提供了一个可移植到任何 Python 解释器的 import 实现。 与使用 Python 以…...

【PyTorch】现代卷积神经网络

文章目录 1. 理论介绍1.1. 深度卷积神经网络&#xff08;AlexNet&#xff09;1.1.1. 概述1.1.2. 模型设计 1.2. 使用块的网络&#xff08;VGG&#xff09;1.3. 网络中的网络&#xff08;NiN&#xff09;1.4. 含并行连结的网络&#xff08;GoogLeNet&#xff09;1.5. 批量规范化…...

用python编写九九乘法表

1 问题 我们在学习一门语言的过程中&#xff0c;都会练习到编写九九乘法表这个代码&#xff0c;下面介绍如何编写九九乘法表的流程。 2 方法 &#xff08;1&#xff09;打开pycharm集成开发环境&#xff0c;创建一个python文件&#xff0c;并编写第一行代码&#xff0c;主要构建…...

Google Gemini 模型本地可视化

Google近期发布了Gemini模型&#xff0c;而且开放了Gemini Pro API&#xff0c;Gemini Pro 可免费使用&#xff01; Gemini Pro支持全球180个国家的38种语言&#xff0c;目前接受文本、图片作为输入并生成文本作为输出。 Gemini Pro的表现超越了其他同类模型&#xff0c;当前版…...

数据修复:.BlackBit勒索病毒来袭,安全应对方法解析

导言&#xff1a; 黑色数字罪犯的新玩具——.BlackBit勒索病毒&#xff0c;近来成为网络安全领域的头号威胁。这种恶意软件以其高度隐秘性和毁灭性而引起广泛关注。下面是关于.BlackBit勒索病毒的详细介绍&#xff0c;如不幸感染这个勒索病毒&#xff0c;您可添加我们的技术服…...

拓扑排序实现循环依赖判断 | 京东云技术团队

本文记录如何通过拓扑排序&#xff0c;实现循环依赖判断 前言 一般提到循环依赖&#xff0c;首先想到的就是Spring框架提供的Bean的循环依赖检测&#xff0c;相关文档可参考&#xff1a; https://blog.csdn.net/cristianoxm/article/details/113246104 本文方案脱离Spring Be…...

Java的NIO工作机制

文章目录 1. 问题引入2. NIO的工作方式3. Buffer的工作方式4. NIO数据访问方式 1. 问题引入 在网络通信中&#xff0c;当连接已经建立成功&#xff0c;服务端和客户端都会拥有一个Socket实例&#xff0c;每个Socket实例都有一个InputStream和OutputStream&#xff0c;并通过这…...

一个简单的光线追踪渲染器

前言 本文参照自raytracing in one weekend教程&#xff0c;地址为&#xff1a;https://raytracing.github.io/books/RayTracingInOneWeekend.html 什么是光线追踪&#xff1f; 光线追踪模拟现实中的成像原理&#xff0c;通过模拟一条条直线在场景内反射折射&#xff0c;最终…...

C++学习笔记(十二)------is_a关系(继承关系)

你好&#xff0c;这里是争做图书馆扫地僧的小白。 个人主页&#xff1a;争做图书馆扫地僧的小白_-CSDN博客 目标&#xff1a;希望通过学习技术&#xff0c;期待着改变世界。 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 文章目录 前言 一、继承关系…...

DC电源模块的设计与制造技术创新

BOSHIDA DC电源模块的设计与制造技术创新 DC电源模块的设计与制造技术创新主要涉及以下几个方面&#xff1a; 1. 高效率设计&#xff1a;传统的DC电源模块存在能量转换损耗较大的问题&#xff0c;技术创新可通过采用高效率的电路拓扑结构、使用高性能的功率开关器件和优化控制…...

Sketch for Mac:实现你的创意绘图梦想的矢量绘图软件

随着数字时代的到来&#xff0c;矢量绘图软件成为了广告设计、插画创作和UI设计等领域中必不可少的工具。在众多矢量绘图软件中&#xff0c;Sketch for Mac&#xff08;矢量绘图软件&#xff09;以其强大的功能和简洁的界面脱颖而出&#xff0c;成为了众多设计师的首选。 Sket…...

ReactNative0.73发布,架构升级与更好的调试体验

这次更新包含了多种提升开发体验的改进&#xff0c;包括&#xff1a; 更流畅的调试体验: 通过 Hermes 引擎调试支持、控制台日志历史记录和实验性调试器&#xff0c;让调试过程更加高效顺畅。稳定的符号链接支持: 简化您的开发工作流程&#xff0c;轻松将文件或目录链接到其他…...

SVN忽略文件的两种方式

当使用版本管理工具时&#xff0c;提交到代码库的文档我们不希望存在把一些临时文件也推送到仓库中&#xff0c;这样就需要用到忽略文件。SVN的忽略相比于GIT稍显麻烦&#xff0c;GIT只需要在.gitignore添加忽略规则即可。而SVN有两种忽略方式&#xff0c;一个是全局设置&#…...

手写VUE后台管理系统10 - 封装Axios实现异常统一处理

目录 前后端交互约定安装创建Axios实例拦截器封装请求方法业务异常处理 axios 是一个易用、简洁且高效的http库 axios 中文文档&#xff1a;http://www.axios-js.com/zh-cn/docs/ 前后端交互约定 在本项目中&#xff0c;前后端交互统一使用 application/json;charsetUTF-8 的请…...

JavaScript装饰者模式

JavaScript装饰者模式 1 什么是装饰者模式2 模拟装饰者模式3 JavaScript的装饰者4 装饰函数5 AOP装饰函数6 示例&#xff1a;数据统计上报 1 什么是装饰者模式 在程序开发中&#xff0c;许多时候都我们并不希望某个类天生就非常庞大&#xff0c;一次性包含许多职责。那么我们就…...

C++学习笔记01

01.C概述&#xff08;了解&#xff09; c语言在c语言的基础上添加了面向对象编程和泛型编程的支持。 02.第一个程序helloworld&#xff08;掌握&#xff09; #define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std;//标准命名空间int main() {//co…...

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…...

业务系统对接大模型的基础方案:架构设计与关键步骤

业务系统对接大模型&#xff1a;架构设计与关键步骤 在当今数字化转型的浪潮中&#xff0c;大语言模型&#xff08;LLM&#xff09;已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中&#xff0c;不仅可以优化用户体验&#xff0c;还能为业务决策提供…...

在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:

在 HarmonyOS 应用开发中&#xff0c;手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力&#xff0c;既支持点击、长按、拖拽等基础单一手势的精细控制&#xff0c;也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档&#xff0c…...

QMC5883L的驱动

简介 本篇文章的代码已经上传到了github上面&#xff0c;开源代码 作为一个电子罗盘模块&#xff0c;我们可以通过I2C从中获取偏航角yaw&#xff0c;相对于六轴陀螺仪的yaw&#xff0c;qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...

MySQL中【正则表达式】用法

MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现&#xff08;两者等价&#xff09;&#xff0c;用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例&#xff1a; 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

dify打造数据可视化图表

一、概述 在日常工作和学习中&#xff0c;我们经常需要和数据打交道。无论是分析报告、项目展示&#xff0c;还是简单的数据洞察&#xff0c;一个清晰直观的图表&#xff0c;往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server&#xff0c;由蚂蚁集团 AntV 团队…...

高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数

高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...

Fabric V2.5 通用溯源系统——增加图片上传与下载功能

fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...

Java + Spring Boot + Mybatis 实现批量插入

在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法&#xff1a;使用 MyBatis 的 <foreach> 标签和批处理模式&#xff08;ExecutorType.BATCH&#xff09;。 方法一&#xff1a;使用 XML 的 <foreach> 标签&#xff…...

探索Selenium:自动化测试的神奇钥匙

目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...