4.3 媒资管理模块 - Minio系统上传图片与视频

文章目录

  • 一、上传图片
    • 1.1 需求分析
    • 1.2 数据模型
      • 1.2.1 media_files 媒资信息表
    • 1.3 准备Minio环境
      • 1.3.1 桶环境
      • 1.3.2 连接Minio参数
      • 1.3.3 Minio配置类
    • 1.4 接口定义
      • 1.4.1 上传图片接口请求参数
      • 1.4.2 上传图片接口返回值
      • 1.4.3 接口代码
    • 1.5 MediaFilesMapper
    • 1.6 MediaFileServiceImpl
    • 1.7 事物优化
    • 1.8 测试
  • 二、上传视频
    • 2.0 响应结果类
    • 2.1 断点续传
    • 2.2 测试Demo
      • 2.2.1 分块测试
      • 2.2.2 合并测试
    • 2.3 测试MinIo
      • 2.3.1 上传分块
      • 2.3.2 合并分块
    • 2.4 上传视频
      • 2.4.1 文件上传前校验
        • 2.4.1.1 BigFilesController
        • 2.4.1.2 MediaFileServiceImpl
      • 2.4.2 上传分块文件
        • 2.4.2.1 BigFilesController
        • 2.4.2.2 MediaFileServiceImpl
      • 2.4.3 合并分块文件
        • 2.4.3.1 BigFilesController
        • 2.4.3.2 MediaFileServiceImpl
      • 2.4.4 测试

一、上传图片

此模块涉及到修改/添加课程,之前已经写过接口了,在下面这篇文章中

3.2 内容管理模块 - 课程分类、新增课程、修改课程-CSDN博客

1.1 需求分析

我们的文件要存储在minio文件系统中

在新增课程界面上传课程图片,也可以修改课程图片

这一步上传图片是请求的媒资服务接口,并不是内容管理服务接口

image-20231222215414543

在课程基本信息中有一个“pic”字段,要存储图片的路径

image-20231222215700731

pic字段值类似“/mediafiles/2022/09/18/a16da7a132559daf9e1193166b3e7f52.jpg”,其中“mediafiles”就是桶,后面就是文件具体的对象名

在我们前端功能.env环境变量配置文件定义了图片服务器地址

如果想要展示图片,那就要将“http://192.168.101.65:9000”拼接上pic字段值

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为什么不将“http://192.168.101.65:9000”直接写在数据库里

因为如果上线之后,地址就不好修改了


为什么要单独设置一个媒资管理服务

①管理文件的信息,我们使用media_files表记录文件信息

不管是图片、视频、文档,全会记录在这个表中

image-20231222223702229

②把文件本身放在分布式文件系统(minio)中管理起来

方便管理,媒资服务就是一个统一的文件管理功能


详细流程

image-20231222223956542

1、前端进入上传图片界面

2、上传图片,请求媒资管理服务。

3、媒资管理服务将图片文件存储在MinIO。

4、媒资管理记录文件信息到数据库。

5、前端请求内容管理服务保存课程信息,在内容管理数据库保存图片地址。

1.2 数据模型

我们怎么区分文件是否已经上传

可以通过文件的MD5值,同一个文件的MD5值相同

前端上传的时候就会把文件MD5值传给我们后台,后台拿着MD5值去数据库查询,如果有这个MD5值了,说明文件已经上传成功了,不需要再上传了

1.2.1 media_files 媒资信息表

image-20231222224302847

file_path存储路径,相当于java代码中的对象名

url:可以通过http访问文件的路径

url = /bucket/+file_path

但是有例外,如果是上传了一个avi视频的话,那file_path里面存的就是avi结尾的视频地址,但是avi的视频最终是不能播放的,我们需要把这个视频进行转码生成MP4格式的视频,此时url就是存储的MP4的视频地址、

如果是图片的话url = /bucket/+file_path这个等式是一个样子的

/**
 * 媒资信息
 */
@Data
@TableName("media_files")
public class MediaFiles implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;

    /**
     * 机构ID
     */
    private Long companyId;

    /**
     * 机构名称
     */
    private String companyName;

    /**
     * 文件名称
     */
    private String filename;

    /**
     * 文件类型(文档,音频,视频)
     */
    private String fileType;

    /**
     * 标签
     */
    private String tags;

    /**
     * 存储目录
     */
    private String bucket;

    /**
     * 存储路径
     */
    private String filePath;


    /**
     * 文件标识
     */
    private String fileId;

    /**
     * 媒资文件访问地址
     */
    private String url;


    /**
     * 上传人
     */
    private String username;

    /**
     * 上传时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createDate;

    /**
     * 修改时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime changeDate;

    /**
     * 状态,1:未处理,视频处理完成更新为2
     */
    private String status;

    /**
     * 备注
     */
    private String remark;

    /**
     * 审核状态
     */
    private String auditStatus;

    /**
     * 审核意见
     */
    private String auditMind;

    /**
     * 文件大小
     */
    private Long fileSize;

}

除此之外还会涉及到课程基本信息表course_base,表中pic字段存储课程图片的路径

image-20231222215700731

1.3 准备Minio环境

1.3.1 桶环境

  • mediafiles桶

    视频以外的文件都存储在这个桶内

image-20231222230701829

  • video 桶

    存放视频

    image-20231222230711368

并且两个桶的Access Policy权限都要改成public

image-20231222230808165

1.3.2 连接Minio参数

在media-service工程中配置minio的相关信息,要配置在nacos中

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.101.65:3306/xcplus_media?serverTimezone=UTC&userUnicode=true&useSSL=false&
    username: root
    password: mysql
  cloud:
   config:
    #配置本地优先
    override-none: true  

minio:
  #地址
  endpoint: http://192.168.101.65:9000 
  #账号
  accessKey: minioadmin 
  #密码
  secretKey: minioadmin 
  #两个桶
  bucket:
    files: mediafiles 
    videofiles: video

本地bootstrap.yaml文件

spring:
  application:
    name: media-service
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      discovery:
        namespace: ${spring.profiles.active}
        group: xuecheng-plus-project
      config:
        namespace: ${spring.profiles.active}
        group: xuecheng-plus-project
        file-extension: yaml
        refresh-enabled: true
        shared-configs:
          - data-id: logging-${spring.profiles.active}.yaml
            group: xuecheng-plus-common
            refresh: true

#profiles默认为dev
  profiles:
    active: dev

1.3.3 Minio配置类

在media-service工程中进行配置

@Configuration
public class MinioConfig {
 
    @Value("${minio.endpoint}")
    private String endpoint;
    @Value("${minio.accessKey}")
    private String accessKey;
    @Value("${minio.secretKey}")
    private String secretKey;

    @Bean
    public MinioClient minioClient() {

        MinioClient minioClient =
                MinioClient.builder()
                        .endpoint(endpoint)
                        .credentials(accessKey, secretKey)
                        .build();
        return minioClient;
    }
}

1.4 接口定义

下面的内容其实是完成标红的地方

image-20231222235649629

做这一部分的时候可能发现course_base课程基本信息表和media_files表没有关联,那怎么将图片的url存放到course_base课程基本信息表中的pic字段呢?

当上传完图片之后,会点击保存,这个时候就会对course_base表中的各种信息进行操作了

image-20231223214409515

但是我感觉没有很好的方法让course_base表

1.4.1 上传图片接口请求参数

和media_file相关的文件上传的基本信息

/**
 * @description 上传普通文件请求参数
 */
@Data
public class UploadFileParamsDto {

    /**
     * 文件名称
     */
    private String filename;


    /**
     * 文件类型(文档,音频,视频)
     */
    private String fileType;
    /**
     * 文件大小
     */
    private Long fileSize;

    /**
     * 标签
     */
    private String tags;

    /**
     * 上传人
     */
    private String username;

    /**
     * 备注
     */
    private String remark;

}

1.4.2 上传图片接口返回值

这些信息其实是文件表中的信息

{
  "id": "a16da7a132559daf9e1193166b3e7f52",
  "companyId": 1232141425,
  "companyName": null,
  "filename": "1.jpg",
  "fileType": "001001",
  "tags": "",
  "bucket": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
  "fileId": "a16da7a132559daf9e1193166b3e7f52",
  "url": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
  "timelength": null,
  "username": null,
  "createDate": "2022-09-12T21:57:18",
  "changeDate": null,
  "status": "1",
  "remark": "",
  "auditStatus": null,
  "auditMind": null,
  "fileSize": 248329
}

根据上面返回信息封装一个Dto

但是我们不会直接使用media_files的实体类,假如前端多需要几个参数的话,我们还需要修改

在media-model工程中添加如下实体类

@Data
public class UploadFileResultDto extends MediaFiles {

}

1.4.3 接口代码

    /**
     * 对于请求内容:Content-Type: multipart/form-data;
     * 前端向后端传输一个文件,那后端程序就属于一个消费者,我们指定一下类型
     * form-data; name="filedata"; filename="具体的文件名称"
     * <p>
     * 我们可以使用@RequestPart指定一下前端向后端传输文件的名称
     * 用MultipartFile类型接收前端向后端传输的文件
     */
    @ApiOperation("上传图片")
    @PostMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {
        //此时已经接收到文件了,目前作为临时文件存储在内存中
        //1.创建一个临时文件,前缀是"minio",后缀是“.temp”
        File tempFile = File.createTempFile("minio", ".temp");
        //2.将上传后的文件传输到临时文件中
        filedata.transferTo(tempFile);
        //3.取出临时文件的绝对路径
        String localFilePath = tempFile.getAbsolutePath();

        Long companyId = 1232141425L; //先写死,写认证授权系统时再进行
        //4.准备上传文件的信息
        UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
        //filedata.getOriginalFilename()获取原始文件名称
        uploadFileParamsDto.setFilename(filedata.getOriginalFilename());
        //文件大小
        uploadFileParamsDto.setFileSize(filedata.getSize());
        //文件类型 001001在数据字典中代表图片
        uploadFileParamsDto.setFileType("001001");
        //调用service上传图片
        return mediaFileService.uploadFile(companyId, uploadFileParamsDto, localFilePath);
    }

1.5 MediaFilesMapper

我们需要保存文件信息

/**
 * 媒资信息 Mapper 接口
 */
public interface MediaFilesMapper extends BaseMapper<MediaFiles> {

}

1.6 MediaFileServiceImpl

媒资管理业务类 - 下面的代码其实就做了上传图片与文件信息入库

    @Autowired
    private MinioClient minioClient;

    //除了视频文件以外的桶
    @Value("${minio.bucket.files}")
    private String bucket_medialFiles;

    //视频文件桶
    @Value("${minio.bucket.videofiles}")
    private String bucket_video;

 /**
     * 上传文件
     *
     * @param companyId           机构id
     * @param uploadFileParamsDto 上传文件信息
     * @param localFilePath       文件磁盘路径
     * @return 文件信息
     */
    @Transactional
    @Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
        //TODO 1.将文件上传到Minio

        //TODO 1.1 获取文件扩展名
        String filename = uploadFileParamsDto.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));

        //TODO 1.2 根据文件扩展名获取mimeType
        String mimeType = this.getMimeType(extension);

        //TODO 1.3 bucket,从nacos中读取
        //TODO 1.4 ObjectName约定在MinIo系统中存储的目录是年/月/日/图片文件
        //得到文件的路径defaultFolderPath
        String defaultFolderPath = this.getDefaultFolderPath();
        //最终存储的文件名是MD5值
        String fileMd5 = this.getFileMd5(new File(localFilePath));
        String ObjectName = defaultFolderPath + fileMd5 + extension;
        //TODO 1.5 上传文件到Minio
        boolean result = this.addMediaFilesToMinIO(localFilePath, bucket_medialFiles, ObjectName, mimeType);

        if (!result) {
            XueChengPlusException.cast("上传文件失败");
        }
        //TODO 2.将文件信息保存到数据库
        MediaFiles mediaFiles = this.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_medialFiles, ObjectName);
        if (mediaFiles == null){
            XueChengPlusException.cast("文件上传后保存信息失败");
        }
        UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
        BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
        return uploadFileResultDto;
    }

    /**
     * @param companyId           机构id
     * @param fileMd5             文件md5值
     * @param uploadFileParamsDto 上传文件的信息
     * @param bucket              桶
     * @param objectName          对象名称
     * @return com.xuecheng.media.model.po.MediaFiles
     * @description 将文件信息添加到文件表
     */

    public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
        //根据文件MD5值向数据库查找文件信息
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles == null) {
            mediaFiles = new MediaFiles();
            BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
            mediaFiles.setId(fileMd5);//文件信息的主键是文件的MD5值
            mediaFiles.setCompanyId(companyId);//机构ID
            mediaFiles.setBucket(bucket);//桶
            mediaFiles.setFilePath(objectName);//对象名
            mediaFiles.setFileId(fileMd5);//file_id字段
            mediaFiles.setUrl("/" + bucket + "/" + objectName);//url
            mediaFiles.setCreateDate(LocalDateTime.now());//上传时间
            mediaFiles.setStatus("1");//状态 1正常 0不展示
            mediaFiles.setAuditStatus("002003");//审核状态 002003审核通过
            int insert = mediaFilesMapper.insert(mediaFiles);
            if (insert <= 0) {
                log.debug("向数据库保存文件失败bucket:{},objectName:{}", bucket, objectName);
            }
        }
        return mediaFiles;
    }


    /**
     * 将文件上传到MinIo
     *
     * @param bucket        桶
     * @param localFilePath 文件在本地的路径
     * @param objectName    上传到MinIo系统中时的文件名称
     * @param mimeType      上传的文件类型
     */
    private boolean addMediaFilesToMinIO(String localFilePath, String bucket, String objectName, String mimeType) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    //桶,也就是目录
                    .bucket(bucket)
                    //指定本地文件的路径
                    .filename(localFilePath)
                    //上传到minio中的对象名,上传的文件存储到哪个对象中
                    .object(objectName)
                    .contentType(mimeType)
                    //构建
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.info("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());
            return false;
        }
    }


    /**
     * 根据扩展名获取mimeType
     *
     * @param extension 扩展名
     */
    private String getMimeType(String extension) {
        if (extension == null) {
            //目的是防止空指针异常
            extension = "";
        }
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        //通用mimeType,字节流
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }

    /**
     * @return 获取文件默认存储目录路径 年/月/日
     */
    private String getDefaultFolderPath() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(new Date()).replace("-", "/") + "/";
    }


    /**
     * 获取文件的md5
     *
     * @param file 文件
     * @return MD5值
     */
    private String getFileMd5(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            return DigestUtils.md5Hex(fileInputStream);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

1.7 事物优化

在下面这篇文章中也有事物的方法,感兴趣的话可以查看一下1.6

Redis - 优惠券秒杀、库存超卖、分布式锁、Redisson

我在uploadFile上传文件的方法中加了@Transactional方法

也就是说在uploadFile方法执行之前就会开启事物

但是addMediaFilesToMinIO方法会通过网络访问Minio分布式文件系统上传文件,但是通过网络访问的话这个时间可长可短,假如说网络访问时间长的话,事物的时间就会比较长,占用数据库资源的时间就会长,极端情况下会导致数据库的连接不够用

所在当方法中有网络访问的话,千万不要使用数据库的@Transactional事物进行控制

所以此时我们在addMediaFilesToDb方法上添加@Transactional控制事物的方法就好了


public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
....
}

但是现在也会出现一个问题,我们在uploadFile方法中调用addMediaFilesToDb方法的时候并不会触发addMediaFilesToDb方法上的@Transactional,这种情况也是事物失效的场景之一

分析一下这种情况出现的原因

在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务

image-20231223224851440

如果在uploadFile方法上没有@Transactional注解,代理对象执行此方法前不进行事务控制

断该方法是否可以事务控制必须保证是通过代理对象调用此方法,且此方法上添加了@Transactional注解

image-20231223224941165

在addMediaFilesToDb方法上添加@Transactional注解,也不会进行事务控制是因为并不是通过代理对象执行的addMediaFilesToDb方法

如下图所示,在addMediaFilesToDb方法中的this指代的并不是代理对象,uploadFile方法中this指代的也不是代理对象

我们在Controller使用@AutoWired的注解引入的MediaFileService对象是代理对象,怎么在调用的时候就不是了呢?

在MediaFileServiceImpl类中this指代的就是当前类,不是代理对象

代理对象其实是在原始对象上包了一层,然后调用原始对象上的方法而已

image-20231223225251724

为了解决这个问题,我们可以在uploadFile方法中使用代理的方式调用addMediaFilesToDb方法,使其能触发@Transactional注解

不同担心循环依赖的问题,spring有三级缓存

@Autowired
MediaFileService currentProxy;

将addMediaFilesToDb方法提成接口

/**
 * @description 将文件信息添加到文件表
 * @param companyId  机构id
 * @param fileMd5  文件md5值
 * @param uploadFileParamsDto  上传文件的信息
 * @param bucket  桶
 * @param objectName 对象名称
 * @return com.xuecheng.media.model.po.MediaFiles
 */

public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName);

调用addMediaFilesToDb方法的代码处改为如下

//TODO 2.将文件信息保存到数据库
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_medialFiles, ObjectName);
if (mediaFiles == null){
    XueChengPlusException.cast("文件上传后保存信息失败");
}

此时就是通过CGLB生成的代理对象,挺完美的

image-20231223231025237

1.8 测试

测试上传文件

image-20231223210201133

image-20231223210009088

二、上传视频

上传视频的位置如下所示

image-20231223231511151

image-20231223232117836

2.0 响应结果类

/**
 * 通用结果类
 */
@Data
@ToString
public class RestResponse<T> {
    /**
     * 响应编码,0为正常,-1错误
     */
    private int code;

    /**
     * 响应提示信息
     */
    private String msg;

    /**
     * 响应内容
     */
    private T result;


    public RestResponse() {
        this(0, "success");
    }

    public RestResponse(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 错误信息的封装
     *
     * @param msg
     * @param <T>
     * @return
     */
    public static <T> RestResponse<T> validfail(String msg) {
        RestResponse<T> response = new RestResponse<T>();
        response.setCode(-1);
        response.setMsg(msg);
        return response;
    }

    public static <T> RestResponse<T> validfail(T result, String msg) {
        RestResponse<T> response = new RestResponse<T>();
        response.setCode(-1);
        response.setResult(result);
        response.setMsg(msg);
        return response;
    }


    /**
     * 添加正常响应数据(包含响应内容)
     *
     * @return RestResponse Rest服务封装相应数据
     */
    public static <T> RestResponse<T> success(T result) {
        RestResponse<T> response = new RestResponse<T>();
        response.setResult(result);
        return response;
    }

    public static <T> RestResponse<T> success(T result, String msg) {
        RestResponse<T> response = new RestResponse<T>();
        response.setResult(result);
        response.setMsg(msg);
        return response;
    }

    /**
     * 添加正常响应数据(不包含响应内容)
     *
     * @return RestResponse Rest服务封装相应数据
     */
    public static <T> RestResponse<T> success() {
        return new RestResponse<T>();
    }


    public Boolean isSuccessful() {
        return this.code == 0;
    }
}

2.1 断点续传

假如说用户上传了一个大文件,但是用户的网不是很好,上传了一部分网断了,这个时候就需要一个断点续传的需求了

通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。

http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传

断点续传:在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性

image-20231224012021229

流程如下

1、前端对文件进行分块

2、前端上传分块文件前请求媒资服务检查文件是否存在,如果已经存在则不再上传

3、如果分块文件不存在则前端开始上传

4、前端请求媒资服务上传分块

5、媒资服务将分块上传至MinIO

6、前端将分块上传完毕请求媒资服务合并分块

7、媒资服务判断分块上传完成则请求MinIO合并文件

8、合并完成校验合并后的文件是否完整,如果不完整则删除文件

2.2 测试Demo

2.2.1 分块测试

/**
 * 测试分块
 */
@Test
public void testChunk() throws Exception {
    //TODO 1.获取源文件
    File sourceFile = new File("E:\\歌.mp4");

    //TODO 2.定义基本参数
    //2.1 分块文件存储路径
    String chunkFilePath = "E:\\chunk\\";
    //2.2 分块文件的大小 1024*1024*1 代表1M,5M的话乘5即可(也就是最小单位是字节byte 1024个byte是1k)
    int chunkSize = 1024 * 1024 * 1;
    //2.3 分块文件大小
    //Math.ceil表示向上取整
    //sourceFile.length()是获取文件的大小是多少byte字节
    int chunkNum = (int) Math.ceil((sourceFile.length() * 1.0) / chunkSize);

    //TODO 3.从源文件中读数据,向分块文件中写数据
    //RandomAccessFile流既可以读又可以写
    //参数一:File类型  参数二:是读(“r”)还是写"rw"
    RandomAccessFile raf_r = new RandomAccessFile(sourceFile, "r");
    //缓存区,1k
    byte[] bytes = new byte[1024];
    //TODO 3.1 创建分块文件
    for (int i = 0; i < chunkNum; i++) {
        File chunkFile = new File(chunkFilePath + i);
        //分块文件写入流
        RandomAccessFile raf_rw = new RandomAccessFile(chunkFile, "rw");
        int len = -1;
        //将数据读取到缓冲区中raf_r.read(bytes)
        while ((len = raf_r.read(bytes)) != -1) {
            //向临时文件中进行写入
            raf_rw.write(bytes, 0, len);
            //如果分块文件chunkFile的大小大于等于我们规定的分块文件的chunkSize大小,就不要再继续了
            if (chunkFile.length() >= chunkSize) {
                break;
            }
        }
        raf_rw.close();
    }
    raf_r.close();
}

image-20231224003333071

2.2.2 合并测试

/**
 * 测试合并
 */
@Test
public void testMerge() throws Exception {
    //TODO 1.基本参数
    //分块文件目录
    File chunkFolder = new File("E:\\chunk\\");
    //源文件
    File sourceFile = new File("E:\\歌.mp4");
    //合并后的文件在哪里
    File mergeFile = new File("E:\\chunk\\歌Copy.mp4");

    //TODO 2.取出所有分块文件,此时的顺序可能是无序的
    File[] files = chunkFolder.listFiles();
    //将数组转换成List
    List<File> fileList = Arrays.asList(files);
    //利用Comparator进行排序
    Collections.sort(fileList, new Comparator<File>() {
        @Override
        public int compare(File o1, File o2) {
            //升序
            return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());
        }
    });

    //TODO 3.合并分块文件
    //缓存区,1k
    byte[] bytes = new byte[1024];

    //向合并分块的流
    RandomAccessFile raf_rw = new RandomAccessFile(mergeFile, "rw");
    for (File file : fileList) {
        //向读取分块文件
        RandomAccessFile raf_r = new RandomAccessFile(file, "r");
        int len = -1;
        while ((len = raf_r.read(bytes)) != -1) {
            raf_rw.write(bytes, 0, len);
        }
        raf_r.close();
    }
    raf_rw.close();

    //TODO 校验是否合并成功
    //合并文件完成后比对合并的文件MD5值域源文件MD5值
    FileInputStream fileInputStream = new FileInputStream(sourceFile);
    FileInputStream mergeFileStream = new FileInputStream(mergeFile);
    //取出原始文件的md5
    String originalMd5 = DigestUtils.md5Hex(fileInputStream);
    //取出合并文件的md5进行比较
    String mergeFileMd5 = DigestUtils.md5Hex(mergeFileStream);

    if (originalMd5.equals(mergeFileMd5)) {
        System.out.println("合并文件成功");
    } else {
        System.out.println("合并文件失败");
    }
}

image-20231224010242972

image-20231224010056328

2.3 测试MinIo

image-20231224012021229

MinioClient minioClient =
        MinioClient.builder()
                //这个地方是运行minio后展示的地址
                .endpoint("http://192.168.101.65:9000")
                //账号和密码
                .credentials("minioadmin", "minioadmin")
                .build();

2.3.1 上传分块

将分块文件上传至MinIo

/**
 * 将分块文件上传到minio
 */
@Test
public void uploadChunk() throws Exception {
    //获取所有的分块文件
    File file = new File("E:\\chunk\\");
    File[] files = file.listFiles();
    for (File chunkFile : files) {
        minioClient.uploadObject(
                UploadObjectArgs.builder()
                        //桶,也就是目录
                        .bucket("testbucket")
                        //指定本地文件的路径
                        .filename(chunkFile.getAbsolutePath())
                        //对象名
                        .object("chunk/"+chunkFile.getName())
                        //构建
                        .build());
        System.out.println("上传分块"+chunkFile.getName()+"成功");
    }
}

image-20231224015333814

2.3.2 合并分块

分块已经在Minio了,我们调用Minio提供的SDK即可合并分块

/**
 * 调用minio接口合并分块
 */
@Test
public void merge() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
    //分块文件集合
    List<ComposeSource> sources = new ArrayList<>();
    sources.add(ComposeSource.builder()
            //分块文件所在桶
            .bucket("testbucket")
            //分块文件名称
            .object("chunk/0")
            .build());
    sources.add(ComposeSource.builder()
            //分块文件所在桶
            .bucket("testbucket")
            //分块文件名称
            .object("chunk/1")
            .build());

    ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
            //指定合并后文件在哪个桶里
            .bucket("testbucket")
            //最终合并后的文件路径及名称
            .object("merge/merge01.mp4")
            //指定分块源文件
            .sources(sources)
            .build();
    //合并分块
    minioClient.composeObject(composeObjectArgs);
}

出现这个文件说明我们分块的文件太小了,要大于5242880byte,也就是5M,我们上传的文件才1048576byte,也就是1M

Minio默认的分块文件大小是5M

image-20231224021335475

2.4 上传视频

  • 前端上传视频之前,需要先检查文件是否已经上传过,我们只需要拿着文件的MD5值去数据库查询即可,如果查到了就说明之前已经上传完成了

  • 假如说文件没有上传过,前端开始对视频进行分块然后将分块传输给后端,但是后端不会立即将分块上传到Minio系统,而是先检查此分块是否从前上传到在Minio系统

我们知道文件的MD5值及分块的序号,就可以知道这个分块在不在Minio系统

因为我们是根据文件的MD5值及分块的序号在Minio系统中存储的

  • 等所有的分块文件都上传完毕后,前端就可以请求后端接口进行合并分块

我们某个文件的分块都是在一个目录下的,也就是说一个分块是在一个目录,不用担心分块文件会重复

2.4.1 文件上传前校验

分为“文件上传前检查文件”与“分块文件上传前的检测”两个接口

2.4.1.1 BigFilesController
/**
 * 1.检查文件数据库有没有
 * 2.如果数据库有了再检查minio系统当用有没有(可能存在脏数据,数据库中有但是minio没有那也要传输)
 */
@ApiOperation(value = "文件上传前检查文件")
@PostMapping("/upload/checkfile")
public RestResponse<Boolean> checkfile(@RequestParam("fileMd5") String fileMd5) {
    return mediaFileService.checkFile(fileMd5);
}

/**
 * 分块在数据库中是不存储的,但是可以向minio中查询分块是否存在
 * minio中有了就不再传了,若没有的话再传
 */
@ApiOperation(value = "分块文件上传前的检测")
@PostMapping("/upload/checkchunk")
public RestResponse<Boolean> checkChunk(@RequestParam("fileMd5") String fileMd5,
                                        @RequestParam("chunk") int chunk) throws Exception {
    return mediaFileService.checkChunk(fileMd5,chunk);
}
2.4.1.2 MediaFileServiceImpl
/**
 * 1.首先查询数据库,如果文件不在数据库中表明文件不在
 * 2.如果文件在数据库中再查询minio系统,
 *
 * @param fileMd5 文件的md5
 * @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在
 * @description 检查文件是否存在
 */
@Override
public RestResponse<Boolean> checkFile(String fileMd5) {
    //TODO 1.查询数据库
    MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
    //TODO 2.如果数据库存在再查询minio
    if (mediaFiles != null) {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                //mediaFiles会有记录
                .bucket(mediaFiles.getBucket())
                .object(mediaFiles.getFilePath())
                .build();
        try {
            FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
            if (inputStream != null) {
                //文件已经存在
                return RestResponse.success(true);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //文件不存在
    return RestResponse.success(false);
}

private String getChunkFileFolderPath(String fileMd5) {
    return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/chunk/";
}

检查分块是否存在

对于Service的checkChunk分块文件校验方法我觉得还有另外一个见解思路

课程中对分块文件的校验思路只是检查一下minio系统中有没有对应的minio文件,如果没有的话就上传分块

但是我觉得这个地方可以改一改

有化成根据fileMd5去数据库查一下是否存在这个文件,然后得到这个文件的getFilePath,查看一下是否有完整的视频文件,如果有的话直接不用检查Minio分块文件是否存在了,假如说Minio没有完整的文件,那再上传对应的分块文件

可能有的人会问,我们明明已经在checkFile中校验了,为什么还要校验?

因为我在学习这个课程的时候我发现一个bug,就是我无法重复上传同一个文件,显然是不合理的

我说的重复上传的意思是毫无征兆的上传不上,前端也不会给什么提示信息,一直卡在百分之九十多的位置不动,为了优化这个地方我才采用了我这个思路

而且我不明白课程中checkFile明明已经校验为true文件存在了,还要进行checkChunk检查分块、合并分块的逻辑

如果是我做前端的话,我就当checkFile返回true时也不进行checkchunk检查分块,更不会合并分块

image-20231224223747158

image-20231224223730483

下面的TODO 1便是我新添加上的,只需要加载Todo2之前即可

2.4.3 合并分块文件模块也要加

//TODO 1.判断fileMd5对应的文件已经合并成一个完整的文件了,如果有了的话,那也不需要检查分块了
//判断Minio系统中是否有已经有合并的文件了,如果有的话没有分块所在路径也无所谓
RestResponse<Boolean> booleanRestResponse = this.checkFile(fileMd5);
if (booleanRestResponse.getResult()){
    //文件已经存在
    return RestResponse.success(true);
}
/**
 * 分块不会存在于数据库,直接查询minio系统即可
 *
 * @param fileMd5    文件的md5
 * @param chunkIndex 分块序号
 * @return com.xuecheng.base.model.RestResponse<java.lang.Boolean> false不存在,true存在
 * @description 检查分块是否存在
 */
@Override
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
    //TODO 2.根据MD5得到分块文件的目录路径
    //分块存储路径:md5前两位为两个目录,MD5值也是一层目录,chunk目录存储分块文件
    String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);

    GetObjectArgs getObjectArgs = GetObjectArgs.builder()
            //视频的桶
            .bucket(bucket_video)
            //文件名是目录路径+分块序号
            .object(chunkFileFolderPath+chunkIndex)
            .build();
    try {
        FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
        if (inputStream != null) {
            //文件已经存在
            return RestResponse.success(true);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return RestResponse.success(false);
}

2.4.2 上传分块文件

2.4.2.1 BigFilesController
@ApiOperation(value = "上传分块文件")
@PostMapping("/upload/uploadchunk")
public RestResponse uploadChunk(@RequestParam("file") MultipartFile file,
                                @RequestParam("fileMd5") String fileMd5,
                                @RequestParam("chunk") int chunk) throws Exception {
    //1.创建一个临时文件,前缀是"minio",后缀是“.temp”
    File tempFile = File.createTempFile("minio", ".temp");
    file.transferTo(tempFile);
    return mediaFileService.uploadChunk(fileMd5,chunk,tempFile.getAbsolutePath());
}
2.4.2.2 MediaFileServiceImpl
/**
 * @param fileMd5            文件md5
 * @param chunk              分块序号
 * @param localChunkFilePath 本地文件路径
 * @return com.xuecheng.base.model.RestResponse
 * @description 上传分块
 */
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
    //TODO 将分块文件上传到minio
    //传空默认返回类型MediaType.APPLICATION_OCTET_STREAM_VALUE application/octet-stream未知流类型
    String mimeType = getMimeType(null);
    //获取分块文件的目录
    String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
    boolean b = this.addMediaFilesToMinIO(localChunkFilePath, bucket_video, chunkFileFolderPath + chunk, mimeType);
    if (!b) {
        //false
        return RestResponse.validfail(false, "上传分块文件{" + fileMd5 + "/" + chunk + "}失败");
    }
    //上传分块文件成功
    return RestResponse.success(true);
}

    /**
     * 将文件上传到MinIo
     *
     * @param bucket        桶
     * @param localFilePath 文件在本地的路径
     * @param objectName    上传到MinIo系统中时的文件名称
     * @param mimeType      上传的文件类型
     */
    private boolean addMediaFilesToMinIO(String localFilePath, String bucket, String objectName, String mimeType) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    //桶,也就是目录
                    .bucket(bucket)
                    //指定本地文件的路径
                    .filename(localFilePath)
                    //上传到minio中的对象名,上传的文件存储到哪个对象中
                    .object(objectName)
                    .contentType(mimeType)
                    //构建
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.info("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());
            return false;
        }
    }

2.4.3 合并分块文件

  • 找到分块文件

  • 调用Minio的SDK,将分块文件进行合并

  • 校验合并后的文件与原文件是否一致

    合并后的文件来源于分块文件,如果上传分块的过程中有问题导致数据丢失,那合并后的文件肯定是不完整的

    合并后的文件与原文件一致,才可以说视频是上传成功的

  • 将文件信息入库

  • 最后清理Minio的分块文件

2.4.3.1 BigFilesController
/**
 * @param fileMd5    文件md5值
 * @param fileName   合并分块之后要入库,fileName原始文件名要写在数据库
 * @param chunkTotal 总共分块数
 */
@ApiOperation(value = "合并文件")
@PostMapping("/upload/mergechunks")
public RestResponse mergeChunks(@RequestParam("fileMd5") String fileMd5,
                                @RequestParam("fileName") String fileName,
                                @RequestParam("chunkTotal") int chunkTotal) throws Exception {
    Long companyId = 1232141425L;
    //文件信息对象
    UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
    uploadFileParamsDto.setFilename(fileName);
    uploadFileParamsDto.setTags("视图文件");
    uploadFileParamsDto.setFileType("001002");//数据字典代码 - 001002代表视频
    return mediaFileService.mergeChunks(companyId, fileMd5, chunkTotal, uploadFileParamsDto);
}
2.4.3.2 MediaFileServiceImpl

其实和2.4.1模块同理

可以在mergeChunks放大TODO1之前下面添加这一段代码

假如说合并后的文件已经存在了,就不需要进行合并了

//TODO 0.如果已经有了合并分块后对应的文件的话,就不用再合并了
//判断Minio系统中是否有已经有合并的文件了,如果有的话没有分块所在路径也无所谓
RestResponse<Boolean> booleanRestResponse = this.checkFile(fileMd5);
if (booleanRestResponse.getResult()){
    //文件已经存在
    return RestResponse.success(true);
}
/**
 * 为什么又companyId 机构ID?
 * 分布式文件系统空间不是随便使用的,比如某个机构传输的课程很多很多,那我们就可以收费了(比如超过1Tb便开始收费)
 * 知道了companyId我们就知道是谁传的,也知道这些机构用了多少GB
 *
 * @param companyId           机构id
 * @param fileMd5             文件md5
 * @param chunkTotal          分块总和
 * @param uploadFileParamsDto 文件信息(要入库)
 * @return com.xuecheng.base.model.RestResponse
 * @description 合并分块
 */
@Override
public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
    //TODO 1.获取所有分块文件
    List<ComposeSource> sources = new ArrayList<>();
    //1.1 分块文件路径
    String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
    for (int i = 0; i < chunkTotal; i++) {
        sources.add(ComposeSource.builder()
                //分块文件所在桶
                .bucket(bucket_video)
                //分块文件名称
                .object(chunkFileFolderPath + i).build());
    }
    //1.2 指定合并后文件存储在哪里
    String filename = uploadFileParamsDto.getFilename();
    String fileExt = filename.substring(filename.lastIndexOf("."));
    //1.3 获取对象存储名
    String filePathByMD5 = this.getFilePathByMD5(fileMd5, fileExt);
    ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
            //指定合并后文件在哪个桶里
            .bucket(bucket_video)
            //最终合并后的文件路径及名称
            .object(filePathByMD5)
            //指定分块源文件
            .sources(sources).build();
    //TODO 2.合并分块
    try {
        minioClient.composeObject(composeObjectArgs);
    } catch (Exception e) {
        e.printStackTrace();
        log.error("合并文件出错:bucket:{},objectName:{},错误信息:{}", bucket_video, filePathByMD5, e.getMessage());
        return RestResponse.validfail(false, "合并文件出错");
    }

    //TODO 3.校验合并后的文件与原文件是否一致
    //3.1校验时先要把文件下载下来
    File tempFile = this.downloadFileFromMinIO(bucket_video, filePathByMD5);
    //3.2 比较原文件与临时文件的MD5值
    //将FileInputStream放在括号里,当try..catch执行结束后会自动关闭流,不用加finally了
    try (FileInputStream fis = new FileInputStream(tempFile)) {
        String mergeFile_md5 = DigestUtils.md5Hex(fis);
        if (!fileMd5.equals(mergeFile_md5)) {
            log.error("校验合并文件md5值不一致,原始文件{},合并文件{}", fileMd5, mergeFile_md5);
            return RestResponse.validfail(false, "文件校验失败");
        }

        //保存一下文件信息 - 文件大小
        uploadFileParamsDto.setFileSize(tempFile.length());

    } catch (IOException e) {
        e.printStackTrace();
        return RestResponse.validfail(false, "文件校验失败");
    }

    //TODO 4.文件信息
    MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_video, filePathByMD5);
    if (mediaFiles == null) {
        return RestResponse.validfail(false, "文件入库失败");
    }

    //TODO 5.清理分块文件
    //5.1获取分块文件路径
    //this.getChunkFileFolderPath(fileMd5);
    this.clearChunkFiles(chunkFileFolderPath, chunkTotal);

    return RestResponse.success(true);
}
       /**
     * 清除分块文件
     *
     * @param chunkFileFolderPath 分块文件路径
     * @param chunkTotal          分块文件总数
     */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {
        //需要参数removeObjectsArgs

        //Iterable<DeleteObject> objects =
        List<DeleteObject> objects = Stream.iterate(0, i -> ++i)
                .limit(chunkTotal)
                //String.concat函数用于拼接字符串
                .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                .collect(Collectors.toList());

        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                //指定要清理的分块文件的桶
                .bucket(bucket_video)
                //需要一个Iterable<DeleteObject>迭代器
                .objects(objects)
                .build();
        //执行了这段方法并没有真正的删除,还需要遍历一下
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
        results.forEach(f->{
            try {
                //get方法执行之后才是真正的删除了
                DeleteError deleteError = f.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
//        或者是下面这种遍历方式,都是可以的
//        for (Result<DeleteError> deleteError:results){
//            DeleteError error = deleteError.get();
//        }

    }

最终合并后的路径是在getFilePathByMD5中

/**
 * @param fileMd5 文件md5值
 * @param fileExt 文件扩展名
 */
private String getFilePathByMD5(String fileMd5, String fileExt) {
    return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
}

2.4.4 测试

  • 首先修改Springboot-web默认上传文件大小

前端对文件分块的大小为5MB,SpringBoot web默认上传文件的大小限制为1MB,这里需要在media-api工程修改配置如下:

spring:
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 50MB
  • max-file-size

    指定单个文件上传的最大大小限制,单个文件的大小如果超过了这个配置的值,将会导致文件上传失败

  • max-request-size

    指定整个 HTTP 请求的最大大小限制,包括所有上传文件和其他请求数据,请求的总大小超过了这个配置的值,将会导致整个请求失败

两个配置项的值可以使用标准的大小单位,比如 KB(千字节)、MB(兆字节)等。在你的例子中,50MB 表示最大文件大小和最大请求大小都被限制为 50 兆字节

  • 点击“上传视频”按钮

image-20231224165736643

  • 点击“添加文件”按钮

image-20231224165809867

  • 上传文件

image-20231224172907840

  • 查看Minio系统

    分块上传挺成功的

image-20231224172946156

  • 合并分块之后

    所有的分块都被清理了,然后合并成一个文件

  • 经过我的2.4.3与2.4.1优化之后,上传挺正常了

image-20231224230839650

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

技术阅读周刊第十一期

技术阅读周刊&#xff0c;每周更新。 历史更新 20231124&#xff1a;第七期20231201&#xff1a;第八期20231215&#xff1a;第十‍期 A Comprehensive guide to Spring Boot 3.2 with Java 21, Virtual Threads, Spring Security, PostgreSQL, Flyway, Caching, Micrometer, O…

第19章总结

一.Java绘图类 1.Graphics类 Graphics类是所有图形上下文的抽象基类&#xff0c;它允许应用程序在组件以及闭屏图像上进行绘制。Graphics类封装了Java支持的基本绘图操作所需的状态信息&#xff0c;主要包括颜色、字体、画笔、文本、图像等。 2.Graphics2D类 Graphics2…

MFC 工具栏

目录 工具栏概述 工具栏的使用 添加工具栏资源 创建&#xff0c;加载工具栏 设置工具栏停靠 工具栏概述 CToolBarCtrl-父类CWnd&#xff0c;封装了关于工具栏控件的各种操作。 CToolBar一父类CControlBar&#xff0c;封装了关于工具栏的操作&#xff0c;以及和框架窗口的…

【力扣周赛】第 373 场周赛(交换得到字典序最小的数组 ⭐分解质因子+前缀和+哈希表)

文章目录 竞赛链接Q1&#xff1a;2946. 循环移位后的矩阵相似检查竞赛时代码——模拟 2947. 统计美丽子字符串 I竞赛时代码——前缀和暴力枚举 Q3&#xff1a;2948. 交换得到字典序最小的数组竞赛时代码——排序后判断相似题目——1202. 交换字符串中的元素&#xff08;使用并查…

【C++练级之路】【Lv.5】动态内存管理(都2023年了,不会有人还不知道new吧?)

目录 一、C/C内存分布二、new和delete的使用方式2.1 C语言内存管理2.2 C内存管理2.2.1 new和delete操作内置类型2.2.2 new和delete操作自定义类型 三、new和delete的底层原理3.1 operator new与operator delete函数3.2 原理总结3.2.1 内置类型3.2.2 自定义类型 四、定位new表达…

MFC读取文件数据,添加信息到列表并保存到文件

打开并读取文件信息 添加&#xff1a; BOOL infoDlg::OnInitDialog() {CDialogEx::OnInitDialog();// TODO: 在此添加额外的初始化AfxMessageBox("欢迎查看学生信息");SetList();return TRUE; // return TRUE unless you set the focus to a control// 异常: OCX 属…

Spark编程语言选择:Scala、Java和Python

在大数据处理和分析领域&#xff0c;Apache Spark已经成为一种非常流行的工具。它提供了丰富的API和强大的性能&#xff0c;同时支持多种编程语言&#xff0c;包括Scala、Java和Python。选择合适的编程语言可以直接影响Spark应用程序的性能、可维护性和开发效率。在本文中&…

Json和Xml

一、前言 学习心得&#xff1a;C# 入门经典第8版书中的第21章《Json和Xml》 二、Xml的介绍 Xml的含义&#xff1a; 可标记性语言&#xff0c;它将数据以一种特别简单文本格式储存。让所有人和几乎所有的计算机都能理解。 XML文件示例&#xff1a; <?xml version"1.…

自动驾驶学习笔记(二十二)——自动泊车算法

#Apollo开发者# 学习课程的传送门如下&#xff0c;当您也准备学习自动驾驶时&#xff0c;可以和我一同前往&#xff1a; 《自动驾驶新人之旅》免费课程—> 传送门 《Apollo开放平台9.0专项技术公开课》免费报名—>传送门 文章目录 前言 感知算法 定位算法 规划算法…

分享70个Java源码总有一个是你想要的

分享70个Java源码总有一个是你想要的 学习知识费力气&#xff0c;收集整理更不易。 知识付费甚欢喜&#xff0c;为咱码农谋福利。 链接&#xff1a;https://pan.baidu.com/s/1s8ZVYHb5B1GgXMlpG-6-Iw?pwd6666 提取码&#xff1a;6666 项目名称 admin、cms、console 等多…

nodejs+vue+微信小程序+python+PHP的4s店客户管理系统-计算机毕业设计推荐

系统的功能结构是系统实现的框架&#xff0c;本系统的主要结构为管理员和用户、员工。管理员的功能为车辆信息管理、用户管理、售后服务管理、售后安排管理、完成售后管理等。 本系统实现了售后的在线申请与处理&#xff0c;方便了用户和管理员、员工三方的利益&#xff0c;提高…

Linux 宝塔mysql莫名其妙数据库不见了恢复数据库

起因&#xff1a;宝塔安装的mysql 线上运行突然表包括库都不见了&#xff0c;想办法恢复数据库 登陆mysql cd /www/server/mysql/binmysql -u root -p查看binlog日志是否打开 show variables like log_%;log_bin如果为 ON 则为开启状态&#xff0c;如果开启了才可以进行下一…

【SD】差异值 生成 同一人物 制作 表情包 【1】

说明&#xff1a;只对AI生成的人物&#xff0c;效果稳定。 Reference差异值 生成表情 首先生成一张图片。 测试命令&#xff1a;1 man,chibi,full body, 模型&#xff1a;envyclarityxl02_v10.safetensors [f6c13197db] 种子&#xff1a;2704867166 》》测试命令&#xff1a…

智能优化算法应用:基于金豺算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于金豺算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于金豺算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.金豺算法4.实验参数设定5.算法结果6.参考文献7.MA…

力扣:51. N 皇后

题目&#xff1a; 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回所有不同的 n 皇后问题 的…

网络7层架构

网络 7 层架构 什么是OSI七层模型&#xff1f; OSI模型用于定义并理解数据从一台计算机转移到另一台计算机&#xff0c;在最基本的形式中&#xff0c;两台计算机通过网线和连接器相互连接&#xff0c;在网卡的帮助下共享数据&#xff0c;形成一个网络&#xff0c;但是一台计算…

Unity重写Inspector简化分组配置文件

Unity重写Inspector简化分组配置文件 重写Inspector创建分组管理配置文件创建修改参数参数对应类工程在我的资源中名为CreateConfig&#xff0c;免费下载 重写Inspector创建分组管理配置文件 创建 修改参数 参数对应类 using UnityEngine;public class GameConfig : Scriptab…

【UML】第13篇 序列图(2/2)——建模的方法

目录 三、序列图建模 3.1 概述 3.2 建模的步骤 3.3 举例说明步骤 1.确定主要场景和流程 2.确定参与的对象 3.绘制序列图 4.注意事项 3.4 特殊的情况 序列图是我个人认为&#xff0c;UML中最重要的图之一。 而且序列图&#xff0c;对于业务建模&#xff0c;也有非常好…

【Filament】立方体贴图(6张图)

1 前言 本文通过一个立方体贴图的例子&#xff0c;讲解三维纹理贴图&#xff08;子网格贴图&#xff09;的应用&#xff0c;案例中使用 6 张不同的图片给立方体贴图&#xff0c;图片如下。 读者如果对 Filament 不太熟悉&#xff0c;请回顾以下内容。 Filament环境搭建绘制三角…

java调用GDAL实现栅格数据的重采样的一种方法

目录 1.关于重采样 1.1概念 1.2用途 1.3常见算法 2.关于GDAL 2.1GDAL中的重采样算法 3.实现重采样 3.1思路 3.2完整代码 3.3使用QGIS验证效果 1.关于重采样 1.1概念 重采样是以原始图像的像元值或者导出的值填充到新的图像的每个像元的的过程。 1.2用途 在地理信…