minio 后端大文件分片上传,合并,删除分片

news2024/9/25 21:24:46

背景

网上大多数minio大文件上传都是采用后台返回前端预上传链接,然后由前端去put请求直接和minio通信上传分片文件,然后调用后台合并分片逻辑来达到快申诉上传的目的,详情可以参考我的上两篇文章

最近有个项目域名是https的,但是上传大文件走https太慢,而且服务器配置很拉跨,https里走http预上传不知道为啥老是报错。所以研究下直接从后台分片,然后逐个上传,然后合并,删除分片。

springboot+elementui

集成minio

 <dependency>
   <groupId>io.minio</groupId>
   <artifactId>minio</artifactId>
   <version>8.3.1</version>
</dependency>

yml配置

minio:
    url: http://127.0.0.1:9000 //用于后台内部
    domain: http://127.0.0.1:9000//用于返回给前端,这个可以改成线上域名
    accessKey: minioadmin
    secretKey: minioadmin
    bucketName: minioBackName
    #默认文件存放路径
    filePath: common/

MinioConfig 配置

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.minio.MinioClient;

/**
 * Minio 配置信息
 *
 * @author 
 */
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig
{
    /**
     * 服务地址
     */
    private String url;
    private String domain;

    public String getDomain() {
        return domain;
    }

    public void setDomain(String domain) {
        this.domain = domain;
    }

    /**
     * 用户名
     */
    private String accessKey;

    /**
     * 密码
     */
    private String secretKey;

    /**
     * 存储桶名称
     */
    private String bucketName;
    /**
     * 文件存储指定位置路径
     */
    private String filePath;

    public String getUrl()
    {
        return url;
    }

    public void setUrl(String url)
    {
        this.url = url;
    }

    public String getAccessKey()
    {
        return accessKey;
    }

    public void setAccessKey(String accessKey)
    {
        this.accessKey = accessKey;
    }

    public String getSecretKey()
    {
        return secretKey;
    }

    public void setSecretKey(String secretKey)
    {
        this.secretKey = secretKey;
    }

    public String getBucketName()
    {
        return bucketName;
    }

    public void setBucketName(String bucketName)
    {
        this.bucketName = bucketName;
    }

    public String getFilePath() {
        return filePath;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }


    @Bean
    public MinioClient getMinioClient()
    {
        return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
    }
}

ISysFileService 文件上传接口

import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 文件上传接口
 *
 * @author 
 */
public interface ISysFileService {

    /**
     * 文件上传接口
     *
     * @param file 上传的文件
     * @return 访问地址
     * @throws Exception
     */
    public String uploadFile(MultipartFile file) throws Exception;
    /**
     * 异步文件上传接口
     *
     * @param file 上传的文件
     * @return 访问地址
     * @throws Exception
     */
    public String uploadFileAsync(MultipartFile file) throws Exception;

}

MinioSysFileServiceImpl 实现类

/**
 * Minio 文件存储
 *
 * @author 
 */
@Primary
@Service
@Slf4j
public class MinioSysFileServiceImpl implements ISysFileService {
    //minio每个分片不能低于5MB,最后一个分片可以不管 13MB文件可分成3个分片 5MB 5MB 3MB
    private static final int PART_SIZE = 5 * 1024 * 1024; // 5MB parts
    /**
     * minio基础参数 配置类
     */
    @Autowired
    private MinioConfig minioConfig;
    /**
     * minio客户端连接 连接minio工具
     */
    @Autowired
    private MinioClient client;

    /**
     * 本地文件上传接口
     *
     * @param file 上传的文件
     * @return 访问地址
     * @throws Exception
     */
    @Override
    public String uploadFile(MultipartFile file) throws Exception {
        
        String fileName = file.getOriginalFilename();//获取文件名称
        fileName = minioConfig.getFilePath()+ DateUtils.getDate()+"/"+fileName.substring(fileName.lastIndexOf("/") + 1, fileName.length());
        long startTime = System.currentTimeMillis()/1000;
        //获取文件流
        InputStream inputStream = file.getInputStream();
        //获取文件大小
        long fileSize = file.getSize();
        //计算分片数量
        int partCount = (int) (fileSize / PART_SIZE);
        if (fileSize % PART_SIZE > 0) {
            partCount++;
        }
        long partTime =  System.currentTimeMillis()/1000;
        System.out.println("分片耗时"+(partTime-startTime));
        //存放分片流
        List<InputStream> parts = new ArrayList<>();
        //存放分片minio地址
        List<String> fileList = new ArrayList<>();
        //分配分片流 
        for (int i = 0; i < partCount; i++) {
            // 每次只需要从原始文件InputStream中读取指定大小的数据即可
            byte[] partData = new byte[PART_SIZE];
            int read = inputStream.read(partData);
            if (read == -1) {
                break; // 文件已经读完了
            }
            // 将读取的数据作为一个新的InputStream添加到parts列表中
            parts.add(new ByteArrayInputStream(partData, 0, read));
        }
        long readTime = System.currentTimeMillis()/1000;
        System.out.println("读取文件耗时"+(readTime-partTime));
        //上传分片流到minio
        for (int i = 0; i < parts.size(); i++) {
            // 构建每个part的object name
            String partObjectName = fileName + ".part" + i;
            fileList.add(partObjectName);
            InputStream partStream = parts.get(i);
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(partObjectName)
                    .stream(partStream, partStream.available(), -1)
                    .contentType(file.getContentType())
                    .build();
            ObjectWriteResponse objectWriteResponse = client.putObject(args);
            //System.out.println("分片上传结果======++++++"+objectWriteResponse);
        }
        long upLoadTime = System.currentTimeMillis()/1000;
        System.out.println("上传分片耗时"+(upLoadTime-readTime));
        //关闭主文件输入流和分片输入流
        inputStream.close();
        for (InputStream part : parts) {
            part.close();
        }
        //获取需要合并的分片组装成ComposeSource
        List<ComposeSource> sourceObjectList = new ArrayList<>(fileList.size());
        for (String chunk : fileList){
            sourceObjectList.add(
                    ComposeSource.builder()
                            .bucket(minioConfig.getBucketName())
                            .object(chunk)
                            .build()
            );
        }
        //合并分片
        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                .bucket(minioConfig.getBucketName())
                //合并后的文件的objectname
                .object(fileName)
                //指定源文件
                .sources(sourceObjectList)
                .build();
        client.composeObject(composeObjectArgs);
        long mergeTime = System.currentTimeMillis()/1000;
        System.out.println("合并分片耗时"+(mergeTime-upLoadTime));
        //删除已经上传的分片,组装成DeleteObject
        List<DeleteObject> collect = fileList.stream().map(DeleteObject::new).collect(Collectors.toList());
        //执行删除
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                .bucket(minioConfig.getBucketName())
                .objects(collect)
                .build();
        Iterable<Result<DeleteError>> results = client.removeObjects(removeObjectsArgs);
        //如果没有下面try的代码,文件史删除不了的,加上下面的代码就可以删除了
        try{
            for (Result<DeleteError> result : results){
                DeleteError deleteError = result.get();
                System.out.println("error in deleteing object"+deleteError.objectName()+";"+deleteError.message());
            }
        }catch (Exception e){
            System.out.println("minio删除文件失败");
            e.printStackTrace();
        }
        long deleteTime = System.currentTimeMillis()/1000;
        System.out.println("删除分片耗时"+(deleteTime-mergeTime));
        return fileName;
    }





 /**
     * 异步上传大文件采用链式
     *
     * @param file
     * @return
     * @throws Exception
     */
    @Override
    public String uploadFileAsync(MultipartFile file) throws Exception {
        long startTimes = System.currentTimeMillis() / 1000;
        assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
        String fileName = FileUploadUtils.extractFilename(file);
        fileName = minioConfig.getFilePath() + DateUtils.getDate() + "/" + fileName.substring(fileName.lastIndexOf("/") + 1, fileName.length());
        InputStream inputStream = file.getInputStream();
        ForkJoinPool pool = new ForkJoinPool();
        // 创建分片流异步执行任务:读取大文件分成N个流
        CompletableFuture<List<InputStream>> createPartNumTask = CompletableFuture.supplyAsync(() -> {
            List<InputStream> parts = new ArrayList<>();
            long fileSize = file.getSize();
            int partCount = (int) (fileSize / PART_SIZE);
            if (fileSize % PART_SIZE > 0) {
                partCount++;
            }
            long startTime = System.currentTimeMillis() / 1000;
            for (int i = 0; i < partCount; i++) {
                // 每次只需要从原始文件InputStream中读取指定大小的数据即可
                byte[] partData = new byte[PART_SIZE];
                int read = 0;
                try {
                    read = inputStream.read(partData);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (read == -1) {
                    break; // 文件已经读完了
                }
                // 将读取的数据作为一个新的InputStream添加到parts列表中
                parts.add(new ByteArrayInputStream(partData, 0, read));
            }
            long endTime = System.currentTimeMillis() / 1000;
            System.out.println(Thread.currentThread() + "执行创建分片流任务耗时->" + (endTime - startTime) + "秒");
            return parts;
        }, pool);
        //createPartNum关联的异步任务的返回值作为方法入参,传入到thenApply的方法中
        //thenApply这里实际创建了一个新的CompletableFuture实例
        String finalFileName = fileName;
        CompletableFuture<List<String>> createUploadTask = createPartNumTask.thenApply((parts) -> {
            long startTime = System.currentTimeMillis() / 1000;
            List<String> fileList = new ArrayList<>();
            for (int i = 0; i < parts.size(); i++) {
                // 构建每个part的object name
                String partObjectName = finalFileName + ".part" + i;
                fileList.add(partObjectName);
                InputStream partStream = parts.get(i);
                PutObjectArgs args = null;
                try {
                    args = PutObjectArgs.builder()
                            .bucket(minioConfig.getBucketName())
                            .object(partObjectName)
                            .stream(partStream, partStream.available(), -1)
                            .contentType(file.getContentType())
                            .build();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    ObjectWriteResponse objectWriteResponse = client.putObject(args);
                } catch (ErrorResponseException e) {
                    e.printStackTrace();
                } catch (InsufficientDataException e) {
                    e.printStackTrace();
                } catch (InternalException e) {
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (InvalidResponseException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (ServerException e) {
                    e.printStackTrace();
                } catch (XmlParserException e) {
                    e.printStackTrace();
                }
            }
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            for (InputStream part : parts) {
                try {
                    part.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            long endTime = System.currentTimeMillis() / 1000;
            System.out.println(Thread.currentThread() + "执行上传分片流任务->" + (endTime - startTime) + "秒");
            return fileList;
        });
        String finalFileName1 = fileName;
        CompletableFuture<List<String>> megreTask = createUploadTask.thenApply((fileList) -> {
            long startTime = System.currentTimeMillis() / 1000;
            List<ComposeSource> sourceObjectList = new ArrayList<>(fileList.size());
            for (String chunk : fileList) {
                sourceObjectList.add(
                        ComposeSource.builder()
                                .bucket(minioConfig.getBucketName())
                                .object(chunk)
                                .build()
                );
            }
            ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    //合并后的文件的objectname
                    .object(finalFileName1)
                    //指定源文件
                    .sources(sourceObjectList)
                    .build();
            try {
                client.composeObject(composeObjectArgs);
            } catch (ErrorResponseException e) {
                e.printStackTrace();
            } catch (InsufficientDataException e) {
                e.printStackTrace();
            } catch (InternalException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (InvalidResponseException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (ServerException e) {
                e.printStackTrace();
            } catch (XmlParserException e) {
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis() / 1000;
            System.out.println(Thread.currentThread() + "执行合并分片任务->" + (endTime - startTime) + "秒");
            return fileList;
        });
        CompletableFuture<Boolean> deleteTask = megreTask.thenApply((fileList) -> {
            long startTime = System.currentTimeMillis() / 1000;
            List<DeleteObject> collect = fileList.stream().map(DeleteObject::new).collect(Collectors.toList());
            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .objects(collect)
                    .build();
            Iterable<Result<DeleteError>> results = client.removeObjects(removeObjectsArgs);
            try {
                for (Result<DeleteError> result : results) {
                    DeleteError deleteError = result.get();
                    System.out.println("error in deleteing object" + deleteError.objectName() + ";" + deleteError.message());
                }
            } catch (Exception e) {
                System.out.println("minio删除文件失败");
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis() / 1000;
            System.out.println(Thread.currentThread() + "执行删除分片任务->" + (endTime - startTime) + "秒");
            return true;
        });
        long endTimes = System.currentTimeMillis() / 1000;
        System.out.println("主线程执行耗时" + (endTimes - startTimes) + "秒");
        return fileName;
    }













}

controller 访问接口

@RestController
public class CommonController
{
    @Value("${minio.domain}")//线上的域名
    private String minioUrl;
    @Value("${minio.bucketName}")//桶名
    private String minioBucketName;

/**
     * 文件上传请求
     */
    @PostMapping("/common/upload/minio")
    public AjaxResult upload(MultipartFile file)
    {
        try {
           String url = sysFileService.uploadFile(file);
           String filePath = minioUrl.+ "/"+minioBucketName+"/"+ url;
           AjaxResult ajax = AjaxResult.success();
           ajax.put("fileName", file.getOriginalFilename());//我这里返回的是视频原来的文件名
           ajax.put("url", filePath);
           return ajax;
        }catch (Exception e)
        {
            log.error("上传文件失败", e);
            return AjaxResult.error(e.getMessage());
        }
    }


/**
     * 异步文件上传请求
     */
    @PostMapping("/common/upload/minioAsync")
    public AjaxResult upload(MultipartFile file)
    {
        try {
            long startTime = System.currentTimeMillis()/1000;
                String url = sysFileService.uploadFileAsync(file);
                System.out.println("文件返回时间_END耗时"+(System.currentTimeMillis()/1000-startTime)+"秒");
           String filePath = minioUrl.+ "/"+minioBucketName+"/"+ url;
           AjaxResult ajax = AjaxResult.success();
           ajax.put("fileName", file.getOriginalFilename());//我这里返回的是视频原来的文件名
           ajax.put("url", filePath);
           return ajax;
        }catch (Exception e)
        {
            log.error("上传文件失败", e);
            return AjaxResult.error(e.getMessage());
        }
    }
}

优化空间

MinioSysFileServiceImpl 实现类可以采用异步多线程的方式去执行,前端调用直接返回文件路径,不用管文件是否上传完,异步执行完成以后 调用mino 判断是否存在该文件,来判断该文件是否上传完成,另外想要实现,秒传,断点续传,只要加上MD5编码和redis就可以实现。后面我会补充

优化一 异步上传

代码我我已经再上面补充过了,主要讲下异步多线程 

CompletableFuturejava.util.concurrent库在java 8中新增的主要工具,同传统的Future相比,其支持流式计算、函数式编程、完成通知、自定义异常处理等很多新的特性

CompletableFuture<Integer> future1 
	= CompletableFuture.supplyAsync(()->{
     	System.out.println("compute 1");
     	return 1;
 });
 CompletableFuture<Integer> future2 
 	= future1.thenApply((p)->{
	     System.out.println("compute 2");
	     return p+10;
 });
 System.out.println("result: " + future2.join());

这个例子中展示了任务链

在上面的示例中,future1通过调用thenApply将后置任务连接起来,并形成future2。该示例的最终打印结果为11,可见程序在运行中,future1的结果计算出来后,会传递给通过thenApply连接的任务,从而产生future2的最终结果为1+10=11。当然,在实际使用中,我们理论上可以无限连接后续计算任务,从而实现链条更长的流式计算。

需要注意的是,通过thenApply连接的任务,当且仅当前置任务计算完成时,才会开始后置任务的计算。因此,这组函数主要用于连接前后有依赖的任务链。

利用异步多线程执行流程截图

看一下minio 里有没有文件

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

# 低代码和无代码开发初探

低代码和无代码开发初探 低代码和无代码都是近年来在软件开发领域兴起的技术趋势&#xff0c;它们旨在提高开发效率、降低开发门槛&#xff0c;让更多人能够参与到软件开发过程中。以下是对低代码和无代码的介绍&#xff1a; 一、低代码 1、低代码定义 低代码开发平台&…

Linux nice/renice 命令 - 进程的NI、PRI属性

进程NI、PRI属性的联系 共同影响进程调度&#xff1a;NI和PRI都是Linux进程调度机制中的重要参数&#xff0c;它们共同决定了进程在CPU资源竞争中的优先级。NI值通过影响PRI值来间接影响进程的调度顺序。NI值可调整以改变PRI值&#xff1a;用户可以通过调整进程的NI值来间接改…

较难!第15届蓝桥杯青少组省赛Scratch中级组编程真题

今天上午第15届蓝桥杯青少组省赛Scratch初级组考完试以后&#xff0c;Scratch实验室就预估今天下午的Scratch中级组比较难&#xff0c;结果不出所料&#xff0c;还是比较有难度&#xff0c;据好几个学生及家长说&#xff0c;好几道题不会做时间不够。 来源&#xff1a;结束啦&a…

三级_网络技术_43_综合题(报文)

一、 某客户机使用DHCP获取IP地址等信息&#xff0c;其获取lP地址过程中捕获的4条报文及对第2条报文分析如下所示。请分析其中的信息&#xff0c;补全内容。 编号 报文摘要 DHCP:Request, Type:DHCP discover DHCP:Reply, Type:DHCP__________ DHCP:Request, Type:DHCP Re…

DBeaver安装使用

文章目录 简介支持的数据库支持的系统 下载安装DBeaver使用修改Maven下载jar地址窗口->首选项连接->驱动->Maven配置仓库地址 选择需要连接的数据库进行连接 简介 DBeaver 是一个通用的数据库管理工具和 SQL 客户端&#xff0c;支持 MySQL, PostgreSQL, Oracle, DB2,…

人生苦短,转行程序员要趁早啊

前言 最近有朋友咨询关于如何自学编程语言的问题&#xff0c;发现要回答这个问题&#xff0c;不是一俩句就可以回答清楚并减少当事人的困惑和迷茫。 笔者不知道提问者是问的学习方法还是学习路径&#xff0c;所以特此写一篇文章&#xff0c;斗胆表达一下我对自学编程的一点点…

解决Gradle下载依赖速度慢的问题

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

盘古信息MES制造执行系统,赋能制造企业智能化运营管理

随着工业智能化的不断深入&#xff0c;MES系统在制造业中扮演着越来越重要的角色。盘古信息自主研发的IMS MES&#xff0c;通过提供包括制造数据管理、计划排程管理、生产调度管理、库存管理、质量管理、人力资源管理、设备管理、采购管理、成本管理、看板管理、生产过程控制、…

TCP粘包和抓包

在 TCP 套接字中&#xff0c;发送和接收缓冲区用于暂存数据&#xff0c;以确保数据的可靠传输。具体来说&#xff0c;TCP 的 socket 收发缓冲区的主要特点和概念如下&#xff1a; 1. 发送缓冲区&#xff08;Send Buffer&#xff09; 定义: 发送缓冲区用于存储待发送的数据。应…

如何在不格式化的情况下解锁 Android 智能手机密码

如果您忘记密码&#xff0c;您的 Android 移动设备将锁定您。发生这种情况时&#xff0c;通常可以通过恢复出厂设置来重新获得对设备的访问权限。可悲的是&#xff0c;这将导致所有数据丢失。下面列出的是解锁锁定的Android 手机而不会丢失任何个人数据的有效方法。 Android 手…

排查Maven问题的步骤

0.检查pom文件完整性 1.检查IDEA中配置是否正确 2.使用清楚工具将所有的lastupdate清除, 3.有些依赖是公司的依赖 —>配置私服 —>拷贝同事仓库,覆盖自己的仓库 4.有了私服地址,但是还是下载不到 —>查看地址是否能访问 —>挂VPN

linux多进程与多线程总结

这里写自定义目录标题 2 linux多进程与多线程2.1 进程间通信2.1.1 管道2.1.2 信号2.1.3 消息队列2.1.4 共享内存 3 线程4 IO多路复用4.1 非阻塞IO4.2 IO多路复用 2 linux多进程与多线程 学习并发程序。 linux系统中&#xff0c;使用树型管理进程。因此进程之间有父子关系。通…

如何使用ssm实现学生公寓管理系统的设计与实现

TOC ssm106学生公寓管理系统的设计与实现jsp 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&#xff0c;…

LeetCode.3146.两个字符串的排列差

题目描述&#xff1a; 给你两个字符串 s 和 t&#xff0c;每个字符串中的字符都不重复&#xff0c;且 t 是 s 的一个排列。 排列差 定义为 s 和 t 中每个字符在两个字符串中位置的绝对差值之和。 返回 s 和 t 之间的 排列差 输入输出示例&#xff1a; 思路一&#xff1…

TMC2209模块开启无限位归零

TMC2209无限位归零配置步骤&#xff1a; 3.1 配置内部采样电阻。具体在GCONF中internal_Rsens 1&#xff1b; 此步发送数据 05 00 80 00 00 00 83 00 3.2 电机电流设置&#xff0c;配置IHOLD_RUN寄存器&#xff1b; 此处发送数据 05 00 90 00 00 16 16 12 3.3 设置失速电流阈…

QT接收并解析GPS模块串口数据

目录 一、QT读取串口数据 二、解析数据 目标&#xff1a; 使用QT&#xff0c;读取gps模块的串口数据&#xff0c;并解析其中的经纬高数据&#xff0c;然后进行处理 一、QT读取串口数据 变量定义 QSerialPort *serial; QSerialPortInfo SerialPortInfo; QByteArray lineData…

Deepin【2】:Deepin系统盘扩容

Deepin【2】&#xff1a;Deepin系统盘扩容 1、进入live系统1.1、live系统入步骤 2、连接网络3、新增系统仓库4、安装gparted应用5、使用gparted进行扩容操作5.1、观察当前分区5.2、压缩data分区5.3、Rootb分区合并空闲空间5.4、Rootb分区压缩空间5.5、Roota合并空闲空间5.6、核…

【学习笔记】STM32F407探索者HAL库开发(二)STM32F4最小系统设计

【学习笔记】STM32F407探索者HAL库开发&#xff08;二&#xff09;STM32F4最小系统设计 0 什么是最小系统1 电源电路1.1 数字部分电源1.2 模拟部分电源1.3 参考电压1.4 VBAT 2 复位电路3 BOOT启动电路&#xff08;F4&#xff09;4 晶振电路4.1 低速晶振4.2 高速晶振 5 下载调试…

计算机操作员试题(公共科目)

计算机操作员试题(公共科目) ★★★ RZer整理 仅供参考 ★★★ 单项选择题 “人的本质”这一概念是人对自身的认识发展到一定阶段形成的,它力图从根本上回答“人是什么”或“什么是人”这一重大问题。马克思主义认为,人的本质( )。 A.永恒不变 B.可随主观意志而任意改变 C…

【Material-UI】Radio Group中的 Color 属性详解

文章目录 一、Radio Group 组件概述1. 组件介绍2. 基本用法 二、Color 属性详解1. Color 属性的作用2. 使用 Color 属性设置颜色3. 自定义颜色 三、Color 属性的实际应用场景1. 品牌一致性2. 状态指示3. 突出特定选项 四、注意事项1. 色彩对比2. 无障碍设计3. 主题定制 五、总结…