MinIO学习文档(Java版)

2023-11-20

目录

一、安装

1、在k8s中安装minio单机版

(1)创建minio名称空间
kubectl create ns minio

我们下面的yaml文件用到了minio名称空间,所以需要先把该名称空间创建出来

(2)minio单机版安装yaml

在安装minio之前,以下几点需要先说明一下

yaml文件内容修改说明:大家只用把写注释的地方改成自己的,其他地方都不用变

安装命令:大家可以把下面yaml文件内容复制到minio.yaml中,然后上传到linux中,之后通过kubectl apply -f minio.yaml命令来安装minio

端口解释:对于端口部分解释一下,5000端口是供浏览器访问UI页面的,而9000端口是供客户端连接的

访问链接:安装完成之后,可以通过http://ip:port来访问minio,其中ip就是虚拟机ip,而端口就是5000端口对应的nodePort端口,比如下面yaml文件中的就是30427

登录信息:登录用户名和密码需要看yaml文件中的MINIO_ROOT_USERMINIO_ROOT_PASSWORD的value值,比如我的就是adminadmin123456

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: minio
  # 名称空间
  namespace: minio
spec:
  # 副本数量,建议1个,集群版可能存在问题
  replicas: 1
  selector:
    matchLabels:
      app: minio
  serviceName: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      containers:
        - command:
            - /bin/sh
            - -c
            - minio server /data --console-address ":5000"
          env:
            - name: MINIO_ROOT_USER
              # 登录用户名,按照自己的来设置
              value: "admin"
            - name: MINIO_ROOT_PASSWORD
              # 登录密码,按照自己的来设置
              value: "admin123456"
          image: minio/minio:RELEASE.2022-03-22T02-05-10Z
          name: minio
          ports:
            - containerPort: 9000
              name: data
              protocol: TCP
            - containerPort: 5000
              name: console
              protocol: TCP
          volumeMounts:
            - mountPath: /data
              name: data
  volumeClaimTemplates:
    - apiVersion: v1
      kind: PersistentVolumeClaim
      metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteMany
        resources:
          requests:
            # MioIO存储空间大小
            storage: 5Gi
        # nfs动态挂载存储类名称
        storageClassName: "managed-nfs-storage-bakckup"

---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: minio
  name: minio
  namespace: minio
spec:
  ports:
    - name: data
      port: 9000
      protocol: TCP
      targetPort: 9000
      # 暴露端口
      nodePort: 30024
    - name: console
      port: 5000
      protocol: TCP
      targetPort: 5000
      # 暴露端口
      nodePort: 30427
  selector:
    app: minio
  type: NodePort

登录之后效果如下图:

在这里插入图片描述

二、代码

1、pom.xml(说明:minio所用依赖)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atguigu</groupId>
    <artifactId>minio-study-springboot</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--minio依赖-->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.4.0</version>
        </dependency>
        <!-- 解决报错:okhttp3.RequestBody.create([BLokhttp3/MediaType;)Lokhttp3/RequestBody -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.3</version>
        </dependency>
        <!--aws 文件上传工具包-->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-s3</artifactId>
            <version>1.12.263</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.73</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.3</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2、application.yml(说明:放置minio连接信息、minio文件上传限制)

# minio文件上传大小限制
spring:
  servlet:
    multipart:
      max-file-size: 1GB
      max-request-size: 1GB

# minio连接配置
minio:
  # minio通信地址
  endpoint: http://192.168.139.128:30024
  # minio登录用户名
  userName: admin
  # minio登录密码
  password: admin123456

3、MinioProperties.java(说明:读取minio连接信息)

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties("minio")
public class MinioProperties {

    /**
     * 访问地址 http://localhost:9000
     */
    private String endpoint;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 密码
     */
    private String password;
}

4、MinioConfig.java(说明:配置minio连接客户端类)

import com.atguigu.miniostudyspringboot.util.ParallelMinioClient;
import com.atguigu.miniostudyspringboot.util.ReflectUtils;
import io.minio.MinioAsyncClient;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * 传统连接MinIO方式
 * @author 明快de玄米61
 * @date   2022/12/15 14:22
 **/
@Data
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class MinioConfig {

    @Resource
    private MinioProperties minioProperties;

    /**
     * 注入minio 客户端
     */
    @Bean
    public MinioClient minioClient() {

        return MinioClient.builder()
                .endpoint(minioProperties.getEndpoint())
                .credentials(minioProperties.getUserName(), minioProperties.getPassword())
                .build();
    }

    @Bean
    @ConditionalOnBean({MinioClient.class})
    @ConditionalOnMissingBean(ParallelMinioClient.class)
    public ParallelMinioClient parallelMinioClient(MinioClient minioClient) {
        MinioAsyncClient asyncClient = ReflectUtils.getFieldValue(minioClient, "asyncClient");
        return new ParallelMinioClient(asyncClient);
    }
}

5、AmazonS3Config.java(说明:配置aws连接minio客户端类)

import com.amazonaws.ClientConfiguration;
import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * AWS连接MioIO方式
 * @author 明快de玄米61
 * @date   2022/12/15 14:23
 **/
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class AmazonS3Config {

    @Resource
    private MinioProperties minioProperties;

    @Bean(name = "amazonS3")
    public AmazonS3 amazonS3() {
        //设置连接时的参数
        ClientConfiguration config = new ClientConfiguration();
        //设置连接方式,可选参数为HTTP和HTTPS
        boolean httpsFlag = minioProperties.getEndpoint().toLowerCase().startsWith("https");
        config.setProtocol(httpsFlag ? Protocol.HTTPS : Protocol.HTTP);
        //设置网络访问超时时间
        config.setConnectionTimeout(5000);
        config.setUseExpectContinue(true);
        AWSCredentials credentials = new BasicAWSCredentials(minioProperties.getUserName(), minioProperties.getPassword());
        //设置Endpoint
        AwsClientBuilder.EndpointConfiguration end_point = new AwsClientBuilder.EndpointConfiguration(minioProperties.getEndpoint(), Regions.US_EAST_1.name());
        AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
                .withClientConfiguration(config)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withEndpointConfiguration(end_point)
                .withPathStyleAccessEnabled(true).build();
        return amazonS3;
    }

}

6、ObjectItem.java(说明:接收Minio返回内容)

import lombok.Data;

@Data
public class ObjectItem {

    private String objectName;
    private Long size;
}

7、PartInfo.java(说明:转换分片信息,返回给前端)

import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
 * 分片信息
 *
 * @author 明快de玄米61
 * @date 2022/12/15 15:59
 */
@Data
@Accessors(chain = true)
public class PartInfo implements Serializable {

    private int partNumber;

    private String etag;

    private String lastModified;

    private Long size;

}

8、Task.java(说明:分片上传任务)

import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
 * 分片上传任务
 *
 * @author 明快de玄米61
 * @date 2022/12/15 14:24
 */
@Data
@Accessors(chain = true)
public class Task implements Serializable {

    // 任务id
    private String id;

    // 桶名称
    private String bucketName;

    // 文件名称
    private String fileName;

    // 存储服务器中的文件绝对路径
    private String remoteFileUrl;

    // 分片上传的uploadId
    private String uploadId;

    // 文件大小(byte)
    private Long fileSize;

    // 分片大小(byte)
    private Long chunkSize;

    // 分片数量
    private Long chunkNum;

    // 上传状态(正在上传:0;已暂停:1;上传成功:2;上传失败:3;终止上传:4)
    // 说明:暂停、开始都是前端控制,后端只是进行状态记录
    private String status;

}

9、AjaxResult.java(说明:控制层统一返回值)

import java.util.HashMap;

/**
 * 操作消息提醒
 *
 * @author ruoyi
 */
public class AjaxResult extends HashMap<String, Object>
{
    private static final long serialVersionUID = 1L;

    /** 状态码 */
    public static final String CODE_TAG = "code";

    /** 返回内容 */
    public static final String MSG_TAG = "msg";

    /** 数据对象 */
    public static final String DATA_TAG = "data";

    /**
     * 状态类型
     */
    public enum Type
    {
        /** 成功 */
        SUCCESS(0),
        /** 警告 */
        WARN(301),
        /** 错误 */
        ERROR(500);
        private final int value;

        Type(int value)
        {
            this.value = value;
        }

        public int value()
        {
            return this.value;
        }
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult()
    {
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param type 状态类型
     * @param msg 返回内容
     * @param data 数据对象
     */
    public AjaxResult(Type type, String msg, Object data)
    {
        super.put(CODE_TAG, type.value);
        super.put(MSG_TAG, msg);
        if (data != null)
        {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static AjaxResult success()
    {
        return AjaxResult.success("操作成功");
    }

    /**
     * 返回成功数据
     *
     * @return 成功消息
     */
    public static AjaxResult success(Object data)
    {
        return AjaxResult.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg)
    {
        return AjaxResult.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data)
    {
        return new AjaxResult(Type.SUCCESS, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @return
     */
    public static AjaxResult error()
    {
        return AjaxResult.error("操作失败");
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(String msg)
    {
        return AjaxResult.error(msg, null);
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult error(String msg, Object data)
    {
        return new AjaxResult(Type.ERROR, msg, data);
    }

}

10、Constants.java(说明:常量类)

/**
 * 常量类
 *
 * @author 明快de玄米61
 * @date 2022/12/15 14:13
 */
public class Constants {

    public static final String BUCKET_NAME = "knowledge";

}

11、ReflectUtils.java(说明:反射工具类)

import org.apache.commons.lang3.Validate;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
 * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
 * 
 * @author ruoyi
 */
@SuppressWarnings("rawtypes")
public class ReflectUtils
{

    /**
     * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
     */
    @SuppressWarnings("unchecked")
    public static <E> E getFieldValue(final Object obj, final String fieldName)
    {
        Field field = getAccessibleField(obj, fieldName);
        if (field == null)
        {
            System.out.println("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
            return null;
        }
        E result = null;
        try
        {
            result = (E) field.get(obj);
        }
        catch (IllegalAccessException e)
        {
            System.out.printf("不可能抛出的异常%s\n", e.getMessage());
        }
        return result;
    }

    /**
     * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.
     * 如向上转型到Object仍无法找到, 返回null.
     */
    public static Field getAccessibleField(final Object obj, final String fieldName)
    {
        // 为空不报错。直接返回 null
        if (obj == null)
        {
            return null;
        }
        Validate.notBlank(fieldName, "fieldName can't be blank");
        for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass())
        {
            try
            {
                Field field = superClass.getDeclaredField(fieldName);
                makeAccessible(field);
                return field;
            }
            catch (NoSuchFieldException e)
            {
                continue;
            }
        }
        return null;
    }

    /**
     * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
     */
    public static void makeAccessible(Field field)
    {
        if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())
                || Modifier.isFinal(field.getModifiers())) && !field.isAccessible())
        {
            field.setAccessible(true);
        }
    }

}

12、ParallelMinioClient.java(说明:将分片方法暴露出来)

import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.XmlParserException;
import io.minio.messages.Part;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CompletableFuture;


/**
 * Created by TD on 2021/10/26
 * 扩展 MinioClient <很多protected 修饰符的分片方法,MinioClient实例对象无法使用,只能自定义类继承使用>
 * minio 大文件分片上传思路:
 * 1. 前端访问文件服务,请求上传文件,后端返回签名数据及uploadId
 * 2. 前端分片文件,携带签名数据及uploadId并发上传分片数据
 * 3. 分片上传完成后,访问合并文件接口,后台负责合并文件。
 */
public class ParallelMinioClient extends MinioAsyncClient {

    public ParallelMinioClient(MinioAsyncClient client) {
        super(client);
    }

    @Override
    public CompletableFuture<CreateMultipartUploadResponse> createMultipartUploadAsync(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.createMultipartUploadAsync(bucketName, region, objectName, headers, extraQueryParams);
    }

    @Override
    public CompletableFuture<UploadPartResponse> uploadPartAsync(String bucketName, String region, String objectName, Object data, long length, String uploadId, int partNumber, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.uploadPartAsync(bucketName, region, objectName, data, length, uploadId, partNumber, extraHeaders, extraQueryParams);
    }

    @Override
    public CompletableFuture<ListPartsResponse> listPartsAsync(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.listPartsAsync(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
    }

    @Override
    public CompletableFuture<ObjectWriteResponse> completeMultipartUploadAsync(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.completeMultipartUploadAsync(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
    }

    @Override
    public CompletableFuture<AbortMultipartUploadResponse> abortMultipartUploadAsync(String bucketName, String region, String objectName, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws InsufficientDataException, InternalException, InvalidKeyException, IOException, NoSuchAlgorithmException, XmlParserException {
        return super.abortMultipartUploadAsync(bucketName, region, objectName, uploadId, extraHeaders, extraQueryParams);
    }
}

13、MinioUtil.java(说明:Minio连接客户端)

import com.atguigu.miniostudyspringboot.entity.ObjectItem;
import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import io.minio.messages.Part;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Component
@Slf4j
public class MinioUtil {

    @Resource
    private MinioClient minioClient;

    @Resource
    private ParallelMinioClient parallelMinioClient;

    /**
     * 判断存储桶是否存在,不存在则创建
     *
     * @param bucketName 存储桶名称
     */
    public void existBucket(String bucketName) {
        try {
            boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!exists) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 查看存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return 桶是否存在
     */
    public boolean bucketExists(String bucketName) {
        boolean found;
        try {
            found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            found = false;
            e.printStackTrace();
        }
        return found;
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 存储桶名称
     * @return 是否创建成功
     */
    public Boolean makeBucket(String bucketName) {
        try {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 删除存储桶
     *
     * @param bucketName 存储桶名称
     * @return 是否删除成功
     */
    public Boolean removeBucket(String bucketName) {
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断对象是否存在
     *
     * @param bucketName 存储桶名称
     * @param objectName MinIO中存储对象全路径
     * @return 对象是否存在
     */
    public boolean existObject(String bucketName, String objectName) {
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 文件上传
     *
     * @param bucketName 存储桶名称
     * @param file       文件
     * @return 桶中位置
     */
    public String upload(String bucketName, MultipartFile file) {
        MultipartFile[] fileArr = {file};
        List<String> fileNames = upload(bucketName, fileArr);
        return fileNames.size() == 0 ? null : fileNames.get(0);
    }

    /**
     * 上传文件
     *
     * @param bucketName 存储桶名称
     * @param fileList   文件列表
     * @return 桶中位置列表
     */
    public List<String> upload(String bucketName, List<MultipartFile> fileList) {
        MultipartFile[] fileArr = fileList.toArray(new MultipartFile[0]);
        return upload(bucketName, fileArr);
    }

    /**
     * description: 上传文件
     *
     * @param bucketName 存储桶名称
     * @param fileArr    文件列表
     * @return 桶中位置列表
     */
    public List<String> upload(String bucketName, MultipartFile[] fileArr) {
        // 保证桶一定存在
        existBucket(bucketName);
        // 执行正常操作
        List<String> bucketFileNames = new ArrayList<>(fileArr.length);
        for (MultipartFile file : fileArr) {
            // 获取桶中文件名称
            // 获取原始文件名称
            String originalFileName = file.getOriginalFilename();
            // 获取当前日期,格式例如:2020/11/04
            String datePath = new SimpleDateFormat("yyyy/MM-dd/HH").format(new Date());
            // 文件名称
            String uuid = UUID.randomUUID().toString().replaceAll("-", "");
            // 获取文件后缀
            String type;
            int index = originalFileName.lastIndexOf(46);
            if (index != -1) {
                type = originalFileName.substring(index + 1);
            } else {
                type = "unkown";
            }
            String bucketFileName = datePath + "/" + uuid + "." + type;

            // 推送文件到MinIO
            try (InputStream in = file.getInputStream()) {
                minioClient.putObject(PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(bucketFileName)
                        .stream(in, in.available(), -1)
                        .contentType(file.getContentType())
                        .build()
                );
            } catch (Exception e) {
                e.printStackTrace();
            }
            bucketFileNames.add(bucketFileName);
        }
        return bucketFileNames;
    }

    /**
     * 文件下载
     *
     * @param bucketName       存储桶名称
     * @param bucketFileName   桶中文件名称
     * @param originalFileName 原始文件名称
     * @param response         response对象
     */
    public void download(String bucketName, String bucketFileName, String originalFileName, HttpServletResponse response) {
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName).object(bucketFileName).build();
        try (GetObjectResponse objResponse = minioClient.getObject(objectArgs)) {
            byte[] buf = new byte[1024];
            int len;
            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
                while ((len = objResponse.read(buf)) != -1) {
                    os.write(buf, 0, len);
                }
                os.flush();
                byte[] bytes = os.toByteArray();
                response.setCharacterEncoding("utf-8");
                //设置强制下载不打开
                response.setContentType("application/force-download");
                // 设置附件名称编码
                originalFileName = new String(originalFileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
                // 设置附件名称
                response.addHeader("Content-Disposition", "attachment;fileName=" + originalFileName);
                // 写入文件
                try (ServletOutputStream stream = response.getOutputStream()) {
                    stream.write(bytes);
                    stream.flush();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取已上传对象的文件流
     *
     * @param bucketName     存储桶名称
     * @param bucketFileName 桶中文件名称
     * @return 文件流
     */
    public InputStream getFileStream(String bucketName, String bucketFileName) throws Exception {
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName).object(bucketFileName).build();
        return minioClient.getObject(objectArgs);
    }

    /**
     * 批量删除文件对象结果
     *
     * @param bucketName      存储桶名称
     * @param bucketFileName 桶中文件名称
     * @return 删除结果
     */
    public DeleteError removeObjectsResult(String bucketName, String bucketFileName) {
        List<DeleteError> results = removeObjectsResult(bucketName, Collections.singletonList(bucketFileName));
        return results.size() > 0 ? results.get(0) : null;
    }

    /**
     * 批量删除文件对象结果
     *
     * @param bucketName      存储桶名称
     * @param bucketFileNames 桶中文件名称集合
     * @return 删除结果
     */
    public List<DeleteError> removeObjectsResult(String bucketName, List<String> bucketFileNames) {
        Iterable<Result<DeleteError>> results = removeObjects(bucketName, bucketFileNames);
        List<DeleteError> res = new ArrayList<>();
        for (Result<DeleteError> result : results) {
            try {
                res.add(result.get());
            } catch (Exception e) {
                e.printStackTrace();
                log.error("遍历删除结果出现错误:" + e.getMessage());
            }
        }
        return res;
    }

    /**
     * 批量删除文件对象
     *
     * @param bucketName      存储桶名称
     * @param bucketFileNames 桶中文件名称集合
     */
    private Iterable<Result<DeleteError>> removeObjects(String bucketName, List<String> bucketFileNames) {
        List<DeleteObject> dos = bucketFileNames.stream().map(DeleteObject::new).collect(Collectors.toList());
        return minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(dos).build());
    }

    /**
     * 查看文件对象
     *
     * @param bucketName 存储桶名称
     * @return 文件对象集合
     */
    public List<ObjectItem> listObjects(String bucketName) {
        Iterable<Result<Item>> results = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).build());
        List<ObjectItem> objectItems = new ArrayList<>();
        try {
            for (Result<Item> result : results) {
                Item item = result.get();
                ObjectItem objectItem = new ObjectItem();
                objectItem.setObjectName(item.objectName());
                objectItem.setSize(item.size());
                objectItems.add(objectItem);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return objectItems;
    }

    /**
     * 获取桶(桶类型:public)中文件访问url
     *
     * @param bucketName     存储桶名称
     * @param bucketFileName 桶中文件名称
     * @return 访问url
     */
    public String getUploadedObjectUrlForPublicBucket(String bucketName, String bucketFileName) {
        return bucketName + "/" + bucketFileName;
    }

    /**
     * 获取桶(不限制桶类型)中文件访问url
     *
     * @param bucketName     存储桶名称
     * @param bucketFileName 桶中文件名称
     * @param expiry         过期时间数量
     * @param timeUnit       过期时间单位
     * @return 访问url
     */
    public String getUploadedObjectUrl(String bucketName, String bucketFileName, Integer expiry, TimeUnit timeUnit) {
        GetPresignedObjectUrlArgs urlArgs = GetPresignedObjectUrlArgs.builder()
                .method(Method.GET)
                .bucket(bucketName)
                .object(bucketFileName)
                .expiry(expiry, timeUnit)
                .build();
        try {
            return minioClient.getPresignedObjectUrl(urlArgs);
        } catch (Exception e) {
            log.error("获取已上传文件的 Url 失败:" + e.getMessage());
            return "";
        }
    }

    /**
     * 创建分片上传请求
     * @author 明快de玄米61
     * @date   2022/12/15 12:47
     * @param  bucketName 桶名称
     * @param  region 一般填null就行
     * @param  objectName MinIO中文件全路径
     * @param  headers 一般只需要设置“Content-Type”
     * @return CreateMultipartUploadResponse对象
     **/
    public CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) {
        // 保证桶一定存在
        existBucket(bucketName);
        // 创建分片上传任务
        try {
            return parallelMinioClient.createMultipartUploadAsync(bucketName, region, objectName, headers, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 前端通过后端上传分片到MinIO
     * @author 明快de玄米61
     * @date   2022/12/15 12:51
     * @param  bucketName MinIO桶名称
     * @param  region 一般填null就行
     * @param  objectName MinIO中文件全路径
     * @param  data 分片文件,只能接收RandomAccessFile、InputStream类型的,一般使用InputStream类型
     * @param  length 文件大小
     * @param  uploadId 文件上传uploadId
     * @param  partNumber 分片编号
     * @param  extraHeaders 一般填null就行
     * @param  extraQueryParams 一般填null就行
     * @return UploadPartResponse对象
     **/
    public UploadPartResponse uploadPart(String bucketName, String region, String objectName, Object data, long length, String uploadId, int partNumber, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
        try {
            return parallelMinioClient.uploadPartAsync(bucketName, region, objectName, data, length, uploadId, partNumber, extraHeaders, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取分片上传地址,前端直接上传分片到MinIO
     * @author 明快de玄米61
     * @date   2022/12/15 12:54
     * @param  bucketName MinIO桶名称
     * @param  ossFilePath MinIO中文件全路径
     * @param  queryParams 查询参数,一般只需要设置“uploadId”和“partNumber”
     * @return 分片上传地址
     **/
    public String getPreSignUploadUrl(String bucketName, String ossFilePath, Map<String, String> queryParams) {
        try {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.PUT)
                            .bucket(bucketName)
                            .object(ossFilePath)
                            .expiry(60 * 60 * 24)
                            .extraQueryParams(queryParams)
                            .build());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取已上传的所有分片列表,可以为前端和completeMultipartUpload方法服务
     * @author 明快de玄米61
     * @date   2022/12/15 12:57
     * @param  bucketName MinIO桶名称
     * @param  region 一般填null就行
     * @param  ossFilePath MinIO中文件全路径
     * @param  maxParts 最大分片数,一般填写10000即可
     * @param  partNumberMarker 直接填0即可
     * @param  uploadId 文件上传uploadId
     * @param  extraHeaders 一般填null就行
     * @param  extraQueryParams 一般填null就行
     * @return ListPartsResponse对象
     **/
    public ListPartsResponse listParts(String bucketName, String region, String ossFilePath, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
        try {
            return parallelMinioClient.listPartsAsync(bucketName, region, ossFilePath, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 合并分片
     * @author 明快de玄米61
     * @date   2022/12/15 12:50
     * @param  bucketName MinIO桶名称
     * @param  region 一般填null就行
     * @param  ossFilePath MinIO中文件全路径
     * @param  uploadId 文件上传uploadId
     * @param  parts 分片信息
     * @param  extraHeaders 一般填null就行
     * @param  extraQueryParams 一般填null就行
     * @return ObjectWriteResponse对象
     **/
    public ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String ossFilePath, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
        try {
            return parallelMinioClient.completeMultipartUploadAsync(bucketName, region, ossFilePath, uploadId, parts, extraHeaders, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 删除MinIO中已有分片
     * @author 明快de玄米61
     * @date   2022/12/15 13:04
     * @param  bucketName MinIO桶名称
     * @param  region 一般填null就行
     * @param  ossFilePath MinIO中文件全路径
     * @param  uploadId 文件上传uploadId
     * @param  extraHeaders 一般填null就行
     * @param  extraQueryParams 一般填null就行
     * @return AbortMultipartUploadResponse对象
     **/
    public AbortMultipartUploadResponse abortMultipartUpload(String bucketName, String region, String ossFilePath, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
        try {
            return parallelMinioClient.abortMultipartUploadAsync(bucketName, region, ossFilePath, uploadId, extraHeaders, extraQueryParams).get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

14、SimpleController.java(说明:测试Minio的普通操作)

import com.atguigu.miniostudyspringboot.util.Constants;
import com.atguigu.miniostudyspringboot.util.MinioUtil;
import io.minio.messages.DeleteError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

/**
 * 普通操作
 * @author 明快de玄米61
 * @date   2022/12/15 14:16
 * @param
 * @return
 **/
@RequestMapping("/simple")
@RestController
public class SimpleController {

    @Resource
    private MinioUtil minIoUtil;

    // 上传文件
    @PostMapping("/uploadFile")
    public String uploadFile(@RequestParam("file") MultipartFile file) {
        return minIoUtil.upload(Constants.BUCKET_NAME, file);
    }

    // 删除文件
    @GetMapping("/deleteFile")
    public DeleteError deleteFile(@RequestParam String bucketFileName) {
        return minIoUtil.removeObjectsResult(Constants.BUCKET_NAME, bucketFileName);
    }

    // 下载文件
    @GetMapping("/downloadFile")
    public void downloadFile(@RequestParam String bucketFileName, @RequestParam String originalFilename, HttpServletResponse response) {
        minIoUtil.download(Constants.BUCKET_NAME, bucketFileName, originalFilename, response);
    }

    // 获取文件临时分享地址
    @GetMapping("/shareUrl")
    public String shareUrl(@RequestParam String bucketFileName) {
        return minIoUtil.getUploadedObjectUrl(Constants.BUCKET_NAME, bucketFileName, 7, TimeUnit.DAYS);
    }
}

15、MinioShardingController.java(说明:测试原生Minio的分片上传操作)

import com.atguigu.miniostudyspringboot.entity.PartInfo;
import com.atguigu.miniostudyspringboot.entity.Task;
import com.atguigu.miniostudyspringboot.util.AjaxResult;
import com.atguigu.miniostudyspringboot.util.Constants;
import com.atguigu.miniostudyspringboot.util.MinioUtil;
import com.google.common.collect.HashMultimap;
import io.minio.*;
import io.minio.messages.Part;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.IOException;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

/**
 * MioIO工具类分片上传
 * 说明:
 * 1、使用MinIO传统依赖即可,不用新增依赖
 * 2、配置MinioConfig即可
 *
 * @author 明快de玄米61
 * @date 2022/12/15 14:15
 */
@RequestMapping("/minio")
@RestController
public class MinioShardingController {

    /** 分片上传任务集合 **/
    private static final List<Task> TASK_LIST = new CopyOnWriteArrayList<>();

    @Resource
    private MinioUtil minIoUtil;

    /**
     * 获取任务id
     * @author 明快de玄米61
     * @date   2022/12/15 14:31
     * @param  fileName 文件原始名称
     * @param  fileSize 文件大小(byte)
     * @param  chunkSize 分片大小(byte),必须大于等于5MB,这是MioIO要求的,否则会报错
     * @return 任务id
     **/
    @GetMapping("/initTask")
    public AjaxResult initTask(
            @RequestParam String fileName,
            @RequestParam Long fileSize,
            @RequestParam Long chunkSize
    ) {
        // 获取uploadId
        String suffix = FilenameUtils.getExtension(fileName);
        String remoteFileUrl = UUID.randomUUID().toString() + "." + suffix;
        String contentType = MediaTypeFactory.getMediaType(remoteFileUrl).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        HashMultimap<String, String> headers = HashMultimap.create();
        headers.put("Content-Type", contentType);
        CreateMultipartUploadResponse response = minIoUtil.createMultipartUpload(
                Constants.BUCKET_NAME,
                null,
                remoteFileUrl,
                headers,
                null
        );
        String uploadId = response.result().uploadId();

        // 创建分片上传任务
        long chunkNum = (long) Math.ceil(fileSize * 1.0 / chunkSize);
        Task task = new Task();
        task.setId(UUID.randomUUID().toString().replace("-", ""))
                .setBucketName(Constants.BUCKET_NAME)
                .setFileName(fileName)
                .setRemoteFileUrl(remoteFileUrl)
                .setUploadId(uploadId)
                .setFileSize(fileSize)
                .setChunkSize(chunkSize)
                .setChunkNum(chunkNum)
                .setStatus("0");
        TASK_LIST.add(task);
        return AjaxResult.success("操作成功" ,task.getId());
    }

    /**
     * 上传分片
     * 说明:前端通过后端上传分片到MinIO,对比:preSignUploadUrl
     * @author 明快de玄米61
     * @date   2022/12/15 14:56
     * @param  file 分片文件
     * @param  id 任务id
     * @param  partNumber 当前分片编号
     **/
    @PostMapping("/uploadPart")
    public AjaxResult uploadPart(
            @RequestPart MultipartFile file,
            @RequestParam String id,
            @RequestParam Integer partNumber
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 上传分片
        try {
            UploadPartResponse response = minIoUtil.uploadPart(
                    task.getBucketName(),
                    null,
                    task.getRemoteFileUrl(),
                    file.getInputStream(),
                    file.getSize(),
                    task.getUploadId(),
                    partNumber,
                    null,
                    null
            );
            return AjaxResult.success();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return AjaxResult.error();
    }

    /**
     * 获取分片上传地址
     * 说明:前端直接上传分片到MinIO,对比:uploadPart
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @param  partNumber 当前分片编号
     * @return
     **/
    @GetMapping("/preSignUploadUrl")
    public AjaxResult preSignUploadUrl(
            @RequestParam String id,
            @RequestParam Integer partNumber
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 获取分片上传地址
        Map<String, String> reqParams = new HashMap<>();
        reqParams.put("uploadId", task.getUploadId());
        reqParams.put("partNumber", partNumber.toString());
        String preSignUploadUrl = minIoUtil.getPreSignUploadUrl(
                Constants.BUCKET_NAME,
                task.getRemoteFileUrl(),
                reqParams
        );
        return AjaxResult.success("操作成功", preSignUploadUrl);
    }

    /**
     * 合并分片
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/merge")
    public AjaxResult merge(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 判断已上传分片数量
        ListPartsResponse listPartsResponse = minIoUtil.listParts(
                Constants.BUCKET_NAME,
                null,
                task.getRemoteFileUrl(),
                10000,
                0,
                task.getUploadId(),
                null,
                null
        );
        List<Part> parts = listPartsResponse.result().partList();
        if (parts.size() != task.getChunkNum()) {
            return AjaxResult.error("分片缺失,请重新上传,任务id:" + id);
        }

        // 合并分片
        ObjectWriteResponse objectWriteResponse = minIoUtil.completeMultipartUpload(
                Constants.BUCKET_NAME,
                null,
                task.getRemoteFileUrl(),
                task.getUploadId(),
                parts.toArray(new Part[0]),
                null,
                null
        );

        // 更新任务状态
        task.setStatus("1");

        return AjaxResult.success();
    }

    /**
     * 终止分片上传
     * 作用:删除任务相关的所有已上传分片,减少MinIO存储空间浪费
     * @author 明快de玄米61
     * @date   2022/12/15 16:08
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/abort")
    public AjaxResult abort(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 终止分片上传
        AbortMultipartUploadResponse response = minIoUtil.abortMultipartUpload(
                Constants.BUCKET_NAME,
                null,
                task.getRemoteFileUrl(),
                task.getUploadId(),
                null,
                null
        );

        // 更新任务状态
        task.setStatus("4");

        return AjaxResult.success();
    }

    /**
     * 获取任务中以上传的分片列表,用于断点续传
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/taskInfo")
    public AjaxResult taskInfo(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 判断文件是否合成
        boolean exist = minIoUtil.existObject(Constants.BUCKET_NAME, task.getRemoteFileUrl());
        if (exist) {
            return AjaxResult.success("文件已经在MinIO中,不需要传输分片了");
        }

        // 查询分片集合
        ListPartsResponse listPartsResponse = minIoUtil.listParts(
                Constants.BUCKET_NAME,
                null,
                task.getRemoteFileUrl(),
                10000,
                0,
                task.getUploadId(),
                null,
                null
        );
        // 返回结果格式转换
        List<PartInfo> result = listPartsResponse.result().partList().stream().map(p -> {
            String lastModified = p.lastModified().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss SSS"));
            return new PartInfo().setSize(p.partSize()).setEtag(p.etag()).setLastModified(lastModified).setPartNumber(p.partNumber());
        }).collect(Collectors.toList());

        return AjaxResult.success(result);
    }

    /**
     * 获取上传分片任务集合
     * @author 明快de玄米61
     * @date   2022/12/15 14:54
     * @return 上传分片任务集合
     **/
    @GetMapping("/getTaskList")
    public List<Task> getTaskList(){
        return TASK_LIST;
    }

    /**
     * 根据任务id获取上传分片任务
     * @author 明快de玄米61
     * @date   2022/12/15 14:55
     * @param  id 任务id
     * @return 上传分片任务
     **/
    @GetMapping("/getTaskById")
    public Task getTaskById(@RequestParam String id){
        for (Task task : TASK_LIST) {
            if (task.getId().equals(id)) {
                return task;
            }
        }
        return null;
    }

}

16、AwsShardingController.java(说明:测试AWS分片上传操作)

import cn.hutool.core.date.DateUtil;
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.*;
import com.atguigu.miniostudyspringboot.entity.Task;
import com.atguigu.miniostudyspringboot.util.AjaxResult;
import com.atguigu.miniostudyspringboot.util.Constants;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.IOException;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

/**
 * AWS分片上传
 * 说明:
 * 1、需要新增以下依赖
 * <dependency>
 *     <groupId>com.amazonaws</groupId>
 *     <artifactId>aws-java-sdk-s3</artifactId>
 *     <version>1.12.263</version>
 * </dependency>
 * 2、新增AmazonS3Config配置类
 *
 * @author 明快de玄米61
 * @date 2022/12/15 14:15
 */
@RequestMapping("/aws")
@RestController
public class AwsShardingController {

    /** 分片上传任务集合 **/
    private static final List<Task> TASK_LIST = new CopyOnWriteArrayList<>();

    @Resource
    private AmazonS3 amazonS3;

    /**
     * 获取任务id
     * @author 明快de玄米61
     * @date   2022/12/15 14:31
     * @param  fileName 文件原始名称
     * @param  fileSize 文件大小(byte)
     * @param  chunkSize 分片大小(byte),必须大于等于5MB,这是MioIO要求的,否则会报错
     * @return 任务id
     **/
    @GetMapping("/initTask")
    public AjaxResult initTask(
            @RequestParam String fileName,
            @RequestParam Long fileSize,
            @RequestParam Long chunkSize
    ) {
        // 获取uploadId
        String suffix = FilenameUtils.getExtension(fileName);
        String remoteFileUrl = UUID.randomUUID().toString() + "." + suffix;
        String contentType = MediaTypeFactory.getMediaType(remoteFileUrl).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(contentType);
        InitiateMultipartUploadResult initiateMultipartUploadResult = amazonS3
                .initiateMultipartUpload(new InitiateMultipartUploadRequest(Constants.BUCKET_NAME, remoteFileUrl).withObjectMetadata(objectMetadata));
        String uploadId = initiateMultipartUploadResult.getUploadId();

        // 创建分片上传任务
        long chunkNum = (long) Math.ceil(fileSize * 1.0 / chunkSize);
        Task task = new Task();
        task.setId(UUID.randomUUID().toString().replace("-", ""))
                .setBucketName(Constants.BUCKET_NAME)
                .setFileName(fileName)
                .setRemoteFileUrl(remoteFileUrl)
                .setUploadId(uploadId)
                .setFileSize(fileSize)
                .setChunkSize(chunkSize)
                .setChunkNum(chunkNum)
                .setStatus("0");
        TASK_LIST.add(task);

        return AjaxResult.success("操作成功" ,task.getId());
    }

    /**
     * 上传分片
     * 说明:前端通过后端上传分片到MinIO,对比:preSignUploadUrl
     * @author 明快de玄米61
     * @date   2022/12/15 14:56
     * @param  file 分片文件
     * @param  id 任务id
     * @param  partNumber 当前分片编号
     **/
    @PostMapping("/uploadPart")
    public AjaxResult uploadPart(
            @RequestPart MultipartFile file,
            @RequestParam String id,
            @RequestParam Integer partNumber
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 上传分片
        try {
            UploadPartRequest request = new UploadPartRequest();
            request.setInputStream(file.getInputStream());
            request.setBucketName(task.getBucketName());
            request.setKey(task.getRemoteFileUrl());
            request.setUploadId(task.getUploadId());
            request.setPartNumber(partNumber);
            request.setPartSize(file.getInputStream().available());
            UploadPartResult uploadPartResult = amazonS3.uploadPart(request);
            return AjaxResult.success(uploadPartResult);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return AjaxResult.error();
    }

    /**
     * 获取分片上传地址
     * 说明:前端直接上传分片到MinIO,对比:uploadPart
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @param  partNumber 当前分片编号
     * @return
     **/
    @GetMapping("/preSignUploadUrl")
    public AjaxResult preSignUploadUrl(
            @RequestParam String id,
            @RequestParam Integer partNumber
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 获取分片上传地址
        // 设置上传分片链接有效时间
        Date expireDate = DateUtil.offsetMillisecond(new Date(), 60 * 60 * 1000);
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(task.getBucketName(), task.getRemoteFileUrl())
                .withExpiration(expireDate).withMethod(HttpMethod.PUT);
        // 设置uploadId
        request.addRequestParameter("uploadId", task.getUploadId());
        // 设置分片数量
        request.addRequestParameter("partNumber", partNumber.toString());
        // 获取分片上传地址
        URL preSignUploadUrl = amazonS3.generatePresignedUrl(request);
        return AjaxResult.success("操作成功", preSignUploadUrl);
    }

    /**
     * 合并分片
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/merge")
    public AjaxResult merge(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 判断已上传分片数量
        ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getRemoteFileUrl(), task.getUploadId());
        PartListing partListing = amazonS3.listParts(listPartsRequest);
        List<PartSummary> parts = partListing.getParts();
        if (parts.size() != task.getChunkNum()) {
            return AjaxResult.error("分片缺失,请重新上传,任务id:" + id);
        }

        // 合并分片
        CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest()
                .withBucketName(task.getBucketName())
                .withKey(task.getRemoteFileUrl())
                .withUploadId(task.getUploadId())
                .withPartETags(parts.stream().map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag())).collect(Collectors.toList()));
        CompleteMultipartUploadResult result = amazonS3.completeMultipartUpload(completeMultipartUploadRequest);

        // 更新任务状态
        task.setStatus("1");

        return AjaxResult.success(result);
    }

    /**
     * 终止分片上传
     * 作用:删除任务相关的所有已上传分片,减少MinIO存储空间浪费
     * @author 明快de玄米61
     * @date   2022/12/15 16:08
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/abort")
    public AjaxResult abort(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 终止分片上传
        AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(task.getBucketName(), task.getRemoteFileUrl(), task.getUploadId());
        amazonS3.abortMultipartUpload(abortMultipartUploadRequest);

        // 更新任务状态
        task.setStatus("4");

        return AjaxResult.success();
    }

    /**
     * 获取任务中以上传的分片列表,用于断点续传
     * @author 明快de玄米61
     * @date   2022/12/15 15:13
     * @param  id 任务id
     * @return
     **/
    @GetMapping("/taskInfo")
    public AjaxResult taskInfo(
            @RequestParam String id
    ) {
        // 获取任务
        Task task = getTaskById(id);
        if (task == null) {
            return AjaxResult.error("不存在该任务,任务id:" + id);
        }

        // 判断文件是否合成
        boolean exist = amazonS3.doesObjectExist(task.getBucketName(), task.getRemoteFileUrl());
        if (exist) {
            return AjaxResult.success("文件已经在MinIO中,不需要传输分片了");
        }

        // 查询分片集合
        ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getRemoteFileUrl(), task.getUploadId());
        PartListing partListing = amazonS3.listParts(listPartsRequest);

        return AjaxResult.success(partListing.getParts());
    }

    /**
     * 获取上传分片任务集合
     * @author 明快de玄米61
     * @date   2022/12/15 14:54
     * @return 上传分片任务集合
     **/
    @GetMapping("/getTaskList")
    public List<Task> getTaskList(){
        return TASK_LIST;
    }

    /**
     * 根据任务id获取上传分片任务
     * @author 明快de玄米61
     * @date   2022/12/15 14:55
     * @param  id 任务id
     * @return 上传分片任务
     **/
    @GetMapping("/getTaskById")
    public Task getTaskById(@RequestParam String id){
        for (Task task : TASK_LIST) {
            if (task.getId().equals(id)) {
                return task;
            }
        }
        return null;
    }

}

三、测试

1、普通操作(对应:SimpleController.java)

1.1、上传文件

在这里插入图片描述

1.2、删除文件

在这里插入图片描述

1.3、下载文件

在这里插入图片描述

1.4、获取文件临时分享地址

在这里插入图片描述

2、Minio分片上传(对应:MinioShardingController)

2.1、初始化分片上传任务

返回值中data里面是任务id
在这里插入图片描述

2.2、分片上传

有多少个分片就需要调用多少次该接口

在这里插入图片描述

2.3、查看任务中已上传分片列表

该接口可以帮助前端判断未上传分片,做到断点续传

在这里插入图片描述

2.4、合并分片

所有分片上传完毕之后,前端调用该接口进行分片合并

在这里插入图片描述

2.5、终止分片

假设我们执行了初始化任务接口、上传分片接口,然后用户想删除正在上传的任务,那就可以调用终止分片接口

在这里插入图片描述

3、AWS分片上传(对应:AwsShardingController)

3.1、初始化分片上传任务

返回值中data里面是任务id
在这里插入图片描述

3.2、分片上传

有多少个分片就需要调用多少次该接口

在这里插入图片描述

3.3、查看任务中已上传分片列表

该接口可以帮助前端判断未上传分片,做到断点续传

在这里插入图片描述

3.4、合并分片

所有分片上传完毕之后,前端调用该接口进行分片合并

在这里插入图片描述

3.5、终止分片

假设我们执行了初始化任务接口、上传分片接口,然后用户想删除正在上传的任务,那就可以调用终止分片接口

在这里插入图片描述

四、资料

  1. MinIO的Java Client API参考文档

五、拓展—分片上传

1、前端分片,后端组合分片(适用范围:本地存储)

推荐大家去看奇闻网盘qiwen-file 项目,该项目中分片上传接口方法如下所示:

类全路径名称: com.qiwenshare.file.controller.FiletransferController

方法名: uploadFile

如果你去看源码,需要拉取这几个项目:

奇文网盘项目实现了前端分片之后,后端实现了以下几种存储方式:

  • 本地存储:后端接收分片,并按照顺序在本地组合分片,组合完成后上传到本地存储位置
  • 阿里云OSS存储:后端接收分片,并上传分片到阿里云,当所有分片接收完成后,调用阿里云组合分片接口进行分片组合
  • FastDFS存储:后端接收分片,并上传分片到FastDFS,当所有分片接收完成后,那就上传完成了
  • MinIO存储:后端接收分片,并按照顺序在本地组合分片,组合完成后在上传到MinIO;这种方式我是不推荐的,原因是MinIO支持分片上传,那我们可以直接把分片上传到MinIO,最终分片上传完成后调用MinIO进行分片合并就好了,可以极大解决本地存储空间

2、前端分片,后端把分片传递给文件存储服务器,调用文件存储服务器的接口组合分片(适用范围:Minio、阿里云OSS存储等)

推荐大家去看暴走的咖喱minio-upload项目,以及明珠spring-boot-minio项目,我们下面来详细介绍一下这两个项目的区别,最后我在补充一些我在项目中用到的其他内容

2.1、minio-upload

项目: https://gitee.com/Gary2016/minio-upload

博客: https://blog.csdn.net/weixin_44359036/article/details/126514643

说明: 这个项目后端没有使用传统连接Minio的方式,而是使用AWS SDK for Java来操作MinIO Server,这种方式也是MinIO官方文档中推荐的方式之一。这个项目我是真正跑起来的,并且实现了分片上传功能,大家按照作者的README.md中的说明文档操作即可,该项目中的一个出色点是获取contentType,这可以让文件上传到文件存储服务器之后保证文件、文件格式都正确,目前没有看到其他项目做到这一点,具体代码如下:

import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;

// 作者在getMediaType方法中写的是key,而我写的是fileName,其实作用是类似的,只要参数中有后缀就好了
String contentType = MediaTypeFactory.getMediaType(fileName).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(contentType);

另外它对流程的把控也是非常nice的,基本流程是创建上传任务、根据分片获取上传地址(上传地址有实效限制,但是这个时间点足够了)、上传分片、合并分片,另外还可以查询已上传分片列表,做到断点续传功能

2.2、spring-boot-minio

项目: https://gitee.com/pearl88/study-demo/tree/master/spring-boot-minio

博客: https://blog.csdn.net/qq_43437874/article/details/123429986

说明: 这个项目后端使用传统连接Minio的方式,虽然在新的minio依赖中,这种方式已经失效了,比如在我上面的minio版本中就已经失效了,但是这种思想依然没有改变的。不过该项目的不好的一点是对contentType的处理,仅仅是简单使用application/octet-stream来代表所有文件的类型,这种方式将会造成上传文本文件多处一些多余内容,所以建议使用上面介绍minio-upload提到的contentType获取方法;另外不好的一点是对上传地址的获取方式,在该项目中是直接在创建上传任务的时候返回所有分片的上传地址,不过这个上传地址有效时间是固定的,并且有最大值,但是用户可能将任务暂停很久很久,所以不建议使用这种方式,而建议使用上传哪个分片就先获取分片上传地址的方式

2.3、公司项目

我在项目中借鉴的minio-upload的做法,也是使用AWS SDK for Java来操作MinIO Server,主要补充两点

1、删除已上传分片

在实际使用中,大文件上传过程中肯定是允许用户中途删除文件的,这种情况下我们需要删除Minio中的分片,避免这些垃圾分片占用存储空间

// 参数解释:MinIO桶名称、文件在MinIO中存储的绝对路径、文件上传uploadId
AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(record.getBucketName(), record.getRemoteFileUrl(), record.getUploadId());
amazonS3.abortMultipartUpload(abortMultipartUploadRequest);

上述方法对标传统连接Minio Server中的该方法:io.minio.S3Base#abortMultipartUploadAsync

2、前端通过后端上传分片到MinIO

我借鉴的是minio-upload项目中的思路,所以我让前端通过Minio返回的分片上传地址上传分片到MinIO,但是前端那边报跨域,另外公司前辈说不建议把MinIO的连接信息暴露给前端,另外以后K8S上部署的MinIO甚至都不会以NodePort方式暴露出来,那这种方式就无法使用了,所以最好还是前端通过后端上传分片到Minio最好,我转念一想大佬说的也有道理,那就我后端中转一下吧,具体代码如下:

Controller@PostMapping("/uploadPart")
@ApiOperation(value = "上传分片", notes = "上传分片")
public CommonRes<UploadPartResult> uploadPart(
        @ApiParam(value = "分片文件", required = true) @RequestPart("file") @Validated MultipartFile file,
        @ApiParam(value = "主键id,用于获取上传信息", required = true) @RequestParam(value = "id") @Validated String id,
        @ApiParam(value = "分片编号", required = true) @RequestParam(value = "partNumber") @Validated() Integer partNumber
) {
    return manager.uploadPart(file, id, partNumber);
}

ServiceImpl@SneakyThrows
@Override
public CommonRes<UploadPartResult> uploadPart(MultipartFile file, String id, Integer partNumber) {
    // 参数判断
    UploadRecord record = service.getById(id);
    if (record == null) {
        // 避免删除接口先调用成功,然后才接收到前端调用上传分片的接口请求,之后导致报错,因此设置状态码是200
        return CommonRes.error(HttpStatusConstants.SUCCESS, "上传记录不存在");
    }

    // 上传分片
    UploadPartRequest request = new UploadPartRequest();
    // 前端上传分片流
    request.setInputStream(file.getInputStream());
    // MioIO桶名称
    request.setBucketName(record.getBucketName());
    // MinIO中的最终合成文件全路径
    request.setKey(record.getRemoteFileUrl());
    // 文件分片上传uploadId
    request.setUploadId(record.getUploadId());
    // 分片编号
    request.setPartNumber(partNumber);
    // 分片大小
    request.setPartSize(file.getInputStream().available());
    // 上传分片到MinIO
    UploadPartResult uploadPartResult = amazonS3.uploadPart(request);

    return CommonRes.ok(uploadPartResult);
}

上述方法对标传统连接Minio Server中的该方法:io.minio.S3Base#uploadPartAsync

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

MinIO学习文档(Java版) 的相关文章

  • Java - 因内存不足错误而关闭

    关于如何最好地处理这个问题 我听到了非常矛盾的事情 并且陷入了以下困境 OOME 会导致一个线程崩溃 但不会导致整个应用程序崩溃 我需要关闭整个应用程序 但不能 因为线程没有剩余内存 我一直认为最佳实践是让它们离开 这样 JVM 就会死掉
  • Guice 忽略注入构造函数参数上的 @Nullable

    我正在使用 Guice v 3 0 并且有一个值被注入到构造函数中 该值可以为 null 因此我在构造函数中使用 Nullable 来自 javax annotations 注释了该参数 public MyClass Parameter1
  • 获取文件的锁

    我想在对特定文件开始 threo read 时获取文件上的锁定 以便其他应用程序无法读取已锁定的文件并希望在线程终止时释放锁定文件 您可以获得一个FileLock https docs oracle com javase 8 docs ap
  • 当路径的点超出视野时,Android Canvas 不会绘制路径

    我在绘制路径时遇到了 Android Canvas 的一些问题 我的情况是 我有一个相对布局工作 如地图视图 不使用 google api 或类似的东西 我必须在该视图上绘制一条路径 canvas drawPath polyPath bor
  • 如何使用 JAVA 代码以编程方式捕获线程转储?

    我想通过 java 代码生成线程转储 我尝试使用 ThreadMXBean 为此 但我没有以正确的格式获得线程转储 因为我们正在使用jstack命令 请任何人提供一些帮助 他们是否有其他方式获取线程转储 使用任何其他 API 我想要的线程转
  • 如何将jscrollpane添加到jframe?

    我有以下源代码 有人可以给我建议如何将 jscrollpane 添加到 jframe 上吗 我尝试了几次将其添加到 jframe 但没有任何进展 它甚至没有显示 public class Form3 JFrame jframe new JF
  • Spring数据中的本机查询连接

    我有课 Entity public class User Id Long id String name ManyToMany List
  • Android蓝牙java.io.IOException:bt套接字已关闭,读取返回:-1

    我正在尝试编写一个代码 仅连接到运行 Android 5 0 KitKat 的设备上的 目前 唯一配对的设备 无论我尝试了多少方法 我仍然会收到此错误 这是我尝试过的最后一个代码 它似乎完成了我看到人们报告为成功的所有事情 有人能指出我做错
  • org/codehaus/plexus/archiver/jar/JarArchiver(不支持的major.minor版本49.0)-Maven构建错误

    下午大家 我在尝试构建项目时收到上述错误 我很确定这与使用 Java 1 6 编译的 Maven 最新更新有关 而我们尝试构建的项目是 1 4 项目 在此之前的插件工作没有问题 因此我将以下内容添加到 POM xml 文件中以尝试强制使用现
  • 从直方图计算平均值和百分位数?

    我编写了一个计时器 可以测量任何多线程应用程序中特定代码的性能 在下面的计时器中 它还会在地图中填充花费了 x 毫秒的调用次数 我将使用这张图作为我的直方图的一部分来进行进一步的分析 例如调用花费了这么多毫秒的百分比等等 public st
  • 从休眠乐观锁定异常中恢复

    我有一个这样的方法 Transactional propagation Propagation REQUIRES NEW public void doSomeWork Entity entity dao loadEntity do some
  • 添加到列表时有没有办法避免循环?

    我想知道这样的代码 List
  • 当 minifyEnabled 为 true 时 Android 应用程序崩溃

    我正在使用多模块应用程序 并且该应用程序崩溃时minifyEnabled true in the installed模块的build gradle 以下是从游戏控制台检索到的反混淆堆栈跟踪 FATAL EXCEPTION Controlle
  • 如何删除日期对象的亚秒部分

    当 SQL 数据类型为时间戳时 java util Date 存储为 2010 09 03 15 33 22 246 如何在存储记录之前将亚秒设置为零 例如 在本例中为 246 最简单的方法是这样的 long time date getTi
  • 如何从日期中删除毫秒、秒、分钟和小时[重复]

    这个问题在这里已经有答案了 我遇到了一个问题 我想比较两个日期 然而 我只想比较年 月 日 这就是我能想到的 private Date trim Date date Calendar calendar Calendar getInstanc
  • 如何停止执行的 Jar 文件

    这感觉像是一个愚蠢的问题 但我似乎无法弄清楚 当我在 Windows 上运行 jar 文件时 它不会出现在任务管理器进程中 我怎样才能终止它 我已经尝试过 TASKKILL 但它对我也不起作用 On Linux ps ef grep jav
  • 避免 Java 中的重复导入:继承导入?

    有没有办法 继承 导入 Example 常见枚举 public enum Constant ONE TWO THREE 使用此枚举的基类 public class Base protected void register Constant
  • 使用Java绘制维恩图

    我正在尝试根据给定的布尔方程绘制维恩图 例如 a AND b AND c我想在 Android 手机上执行此操作 因此我需要找到一种使用 Java 来执行此操作的方法 我找到了一个完美的小部件 它可以完成我在这方面寻找的一切布尔代数计算器
  • 如何让 Emma 或 Cobertura 与 Maven 一起报告其他模块中源代码的覆盖率?

    我有一个带有 Java 代码的多模块 Maven 设置 我的单元测试在其中一个模块中测试多个模块中的代码 当然 这些模块具有相互依赖性 并且在测试执行之前根据需要编译所有相关模块中的代码 那么 如何获得整个代码库覆盖率的报告 注意 我不是问
  • Java 的 PriorityQueue 与最小堆有何不同?

    他们为什么命名PriorityQueue如果你不能插入优先级 它看起来与堆非常相似 有什么区别吗 如果没有区别那为什么叫它PriorityQueue而不是堆 默认的PriorityQueue是用Min Heap实现的 即栈顶元素是堆中最小的

随机推荐

  • MATLAB-DL6

    MATLAB DL6 步骤 交互式迁移貌似2020a才有 学会用analyze network 命令行式 迁移学习 冻结 freezeWeights createLgraphUsingConnections 数据增强 学习参数 函数大杂烩
  • SQL求解用户连续登录天数

    数据分析面试过程中 一般都逃不掉对SQL的考察 可能是笔试的形式 也可能是面试过程中面试官当场提问 当场在纸上写出 或者简单说一下逻辑 今天 就来分享一道面试中常常被问到的一类SQL问题 连续问题 无论是什么样的场景 只要是 连续 问题 那
  • TCP/IP协议之服务器端——华清远见

    咳咳咳 今天也是认真学习的一天 一 TCP IP协议是什么 TCP协议是一种以固连线为基础的协议 它提供两台计算机之间可靠的数据传送 TCP可以保证从一端数据传至连接的另一端时 数据能够确实送达 TCP协议适合可靠性比较高的场合 就像拨打电
  • 队列的几种实现方式

    队列简介 队列是一种特殊的线性表 特殊之处在于它只允许在表的前端 front 进行删除操作 而在表的后端 rear 进行插入操作 和栈一样 队列是一种操作受限制的线性表 进行插入操作的端称为队尾 进行删除操作的端称为队头 队列是一种最常用的
  • Android10(Q)系统源码编译

    Android10系统编译 一 硬件环境 二 软件环境 三 开始编译 四 遇到问题 一 硬件环境 在ubuntu18 04系统中下载编译android10 Q 源码需要如下条件 1 至少4G内存 小于4G内存编译源码期间的等待将会是很痛苦的
  • 【数学建模】数据处理问题

    一 插值与拟合 常用于数据的补全以及趋势分析 1 插值 总的思想 就是利用函数f x 若干已知点的函数值 求出适当的特定函数g x 这样f x 其他未知点上的值 就可以用g x 在这一点的值来近似 这种通过已知求未知的方法称为 插值 插值方
  • mysql知识系列:查看用户密码、修改用户密码,对网上“update user set authentication_string=‘123456’ where user=‘root’;”纠错

    说明 博主用的是mysql8 0 18 网上在找回mysql密码 清一色的教程都是修改root用户的密码 并且使用 update user set authentication string 123456 where user root 博
  • Keycloak概述

    这里写自定义目录标题 Keycloak概述 Single Sign On Kerberos 社交登录 用户合并 客户端适配 管理控制台 用户管理控制台 标准协议 授权服务 Getting Started Keycloak概述 keycloa
  • FPN网络详解

    1 特征金字塔 特征金字塔 Feature Pyramid Networks FPN 的基本思想是通过构造一系列不同尺度的图像或特征图进行模型训练和测试 目的是提升检测算法对于不同尺寸检测目标的鲁棒性 但如果直接根据原始的定义进行FPN计算
  • mysql报错ERROR 1356 (HY000): View ‘mysql.user‘ references invalid table(s) or column(s) or function(s)

    当您在使用 UPDATE user SET password PASSWORD newpassword WHERE User root 命令时提示 ERROR 1356 HY000 View mysql user references in
  • c语言数组下标和指针,C语言 数组 下标与指针 效率解析

    以字符串拷贝函数为例 解析数组中下标与指针的效率情况 指针的效率至少和下标相同 原因参考C下标的实现原理 注意编译器差异 因为部分编译器针对下标设置了特殊汇编指令 不做考虑 define SIZE 50 int x SIZE int y S
  • SQL中join group by having max() 时转Linq

    本来开发时有一个分组聚合的脚本 比较复杂 为了笔记效果 所以将脚本做一个简化 本来库里有两个表TableA和TableB 两个表的主键做如下关联 TableA的主键ID为TableB的外键Aid SELECT a Id a Name b I
  • 【Android11系统开发】上层app通过AIDL监听framework数据

    一 适用场景 在Android系统开发中 需要监听按键 触摸 或者可见窗口大小变化等需求时 你会考虑什么方法来实现呢 通过广播的方式可以实现 但是效果可能并不好 AIDL可以实现跨进程通讯 可以解决以上需求 下面重点分析下如何具体实现 以实
  • Node.js事件循环

    在 Node js 中 事件循环是用来处理非阻塞 I O 的基础 这意味着在 Node js 中 用户代码不会因为等待 I O 操作而停止执行 而是在 I O 操作完成后被通知 Node js 中的事件循环的工作方式有以下几种 首先 Nod
  • 【elementplus】body设置zoom后,el-table开启show-overflow-tooltip后,表格的tooltip显示会错位的解决方案

    由于我的项目是无法避免使用zoom 所以只记录zoom后的解决方案 示例 明明划过的是第一行 tooltip却显示到了第四行的位置 正确显示 划过第一行 tooltip显示在第一行的位置 代码 使用transform属性来修复el tabl
  • JavaScript 实现html导出为PDF文件

    相信各位前端工程狮们在一些报表项目 管理系统项目中都会遇到在这样的需求 申请报 表格 简历等等图文信息有导出为PDF文件 下面是记录我在项目中完成该需求的代码dome 发布出来也是希望对大家有些帮助 1 整体思路 将HTML元素打印或导出为
  • 【满分】【华为OD机试真题2023 JS】统计匹配的二元组个数

    华为OD机试真题 2023年度机试题库全覆盖 刷题指南点这里 统计匹配的二元组个数 知识点数组 时间限制 1s 空间限制 32MB 限定语言 不限 题目描述 给定两个数组A和B 若数组A的某个元素A i 与数组B中的某个元素B j 满足 A
  • 函数getopt(),及其参数optind

    getopt被用来解析命令行选项参数 转载地址 http hi baidu com xlt1888 blog item 703148383008492670cf6c2d html include
  • java属于什么语言_java是什么语言 ?是什么系统?

    一开始了解计算机这个专业 大家都会经常性听到Java这一词语 那么大家有真正的了解什么是Java吗 Java是属于什么语言呢 JAVA语言 其实是混合型的一种语言 Java语言是一个支持网络计算的面向对象程序设计语言 Java语言吸收了Sm
  • MinIO学习文档(Java版)

    目录 一 安装 1 在k8s中安装minio单机版 1 创建minio名称空间 2 minio单机版安装yaml 二 代码 1 pom xml 说明 minio所用依赖 2 application yml 说明 放置minio连接信息 mi