Java でのマルチパート アップロードの実装

Java でのマルチパート アップロードの実装

1: 背景

Web 上で大きなファイルをアップロードおよびダウンロードするには、ファイルをオブジェクト ストレージにアップロードする必要があります。大きなファイルのアップロードには次の問題点があります。

  1. ファイル アップロード タイムアウト: その理由は、フロントエンド リクエスト フレームワークが最大リクエスト期間を制限していること、バックエンドがインターフェイス アクセスのタイムアウトを設定していること、または nginx (または他のプロキシ/ゲートウェイ) が最大リクエスト期間を制限していることです。
  2. ファイル サイズが制限を超えています: バックエンドが 1 つのリクエストのサイズを制限していることが原因です。通常、nginx とサーバーによってこの制限が課されます。
  3. アップロードに時間がかかりすぎる
  4. ネットワークのさまざまな理由によりアップロードに失敗しました。失敗後は最初からやり直す必要があります。

2: 解決策

1. 全体計画

1. フロントエンドは、コードで設定されたフラグメント サイズに従って、アップロードされたファイルをいくつかの小さなファイルに分割し、複数のリクエストで連続してアップロードします。その後、バックエンドはファイルのフラグメントを結合して完全なファイルとしてアップロードします。 。

2. 特定のフラグメントのアップロードに失敗しても、他のファイルのフラグメントには影響しません。失敗した部分を再アップロードするだけで済みます。アップロードされたスライスに関連する情報を維持するためのテーブルを設計する必要があります。

2. コード例

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ShardingFileDTO {
    //文件名称(包含文件后缀)
    private String fileName;
    //文件总大小 MB
    private String size;
    //文件总分片数
    private int shardTotal;
    //分片文件索引下标
    private int shardIndex;
    //文件后缀,视频后缀为mp4,图片则为jpg等
    private String suffix;
    //唯一标识
    private String onlyCode;
}

画像-20230904093947026

package com.xxy.demotest.controller.ShardingFile;

import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.xxy.demotest.controller.ShardingFile.model.ShardingFileDTO;
import com.xxy.demotest.haikang.aliyun.ALiYun;
import com.xxy.demotest.result.baseresult.BaseResponse;
import com.xxy.demotest.utils.WorkUtil.FileUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @beLongProjecet: demo-test
 * @beLongPackage: com.xxy.demotest.controller.ShardingFile
 * @author: xxx
 * @createTime: 2023/09/01 15:16
 * @description: 分片文件上传
 * @version: v1.0
 */
@RestController
@RequestMapping("sharding")
@RequiredArgsConstructor
@Slf4j
public class ShardingFileController {

    public static final String shardPath="D:\\test\\sharding\\";
    public static final String savePath="D:\\test\\save\\";

    private static void excuteFile(ShardingFileDTO dto, MultipartFile multipartFile) throws IOException {
        log.info("文件分片上传请求开始,请求参数: {}", JSON.toJSONString(dto));
        //获取本地文件夹地址
        String fileFolderPath = savePath + dto.getOnlyCode();
        log.info("本地文件夹地址,fileFolder的值为:{}", fileFolderPath);
        //如果目标文件夹不存在,则直接创建一个
        FileUtil.createFolder(fileFolderPath);

        //本地文件全路径
        String fileFullPath =fileFolderPath + File.separator+ dto.getFileName()+"_"+ dto.getShardIndex()+"."+ dto.getSuffix();
        log.info("本地文件全路径,fileFullPath的值为:{}", fileFullPath);
        //将分片文件保存到指定路径
        multipartFile.transferTo(new File(fileFullPath));
        //更新到文件上传表中
        //判断当前分片索引是否等于分片总数,如果等于分片总数则执行文件合并
        if (dto.getShardIndex()==dto.getShardTotal()) {
            //文件合并
            log.info("文件分片合并开始");
            File dirFile = new File(fileFolderPath);
            if (!dirFile.exists()) {
                throw new RuntimeException("文件不存在");
            }
            //分片上传的文件已经位于同一个文件夹下,方便寻找和遍历(当文件数大于十的时候记得排序用冒泡排序确保顺序是正确的)
            List<String> filePaths = FileUtil.listFiles(fileFolderPath);
            if (CollectionUtils.isNotEmpty(filePaths)) {
                //将此里面文件按照索引进行排序
                log.info("filePaths的值为:{}", filePaths);
                // 使用自定义的Comparator来对文件路径进行排序
                Collections.sort(filePaths, new FilePathComparator());
                //进行合并,顺序按照索引进行合并
                String mergedFilePath =fileFolderPath+File.separator+dto.getFileName();
                log.info("生成新的文件的路径,mergedFilePath的值为:{}", mergedFilePath);
                mergeFiles(filePaths, mergedFilePath);
                //合并完成将新文件上传到对象存储中
                String upload = ALiYun.upload(FileUtil.fileToMultipartFile(new File(mergedFilePath)));
                log.info("文件最终访问地址,upload的值为:{}", upload);
                //可以异步
                //删除所有临时切片文件
                deleteFolderAndSubfolders(fileFolderPath);
                //删除所有切片
                deleteFolderAndSubfolders(shardPath+dto.getOnlyCode());
            }

        }
    }


    public static void main(String[] args) {

        String fastUUID = IdUtil.fastSimpleUUID();
        //String sourceFilePath = "C:\\Users\\wonder\\Desktop\\ai测试图片\\切片文件上传\\metacosmic_conference.zip"; // 源文件路径
        String sourceFilePath = "C:\\Users\\wonder\\Desktop\\ai测试图片\\人像.png"; // 源文件路径
        String outputDirectory = shardPath + fastUUID;
        FileUtil.createFolder(outputDirectory);

        //封装dto参数
        ShardingFileDTO shardingFileDTO = new ShardingFileDTO();
        shardingFileDTO.setFileName(new File(sourceFilePath).getName());
        shardingFileDTO.setSize("10MB");

        shardingFileDTO.setSuffix(getFileExtension(sourceFilePath));
        shardingFileDTO.setOnlyCode(fastUUID);
        long sliceSize = 5 * 1024 * 1024; // 切片大小,这里设置为5MB
        try {
            File sourceFile = new File(sourceFilePath);
            String fileName = sourceFile.getName();
            int lastDotIndex = fileName.lastIndexOf('.');
            String suffix = fileName.substring(lastDotIndex + 1);

            long fileSize = sourceFile.length(); // 获取文件大小
            int sliceNumber = (int) Math.ceil((double) fileSize / sliceSize); // 计算切片数量
            log.info("共切割成 " + sliceNumber + " 个文件切片");
            shardingFileDTO.setShardTotal(sliceNumber);
            FileInputStream fis = new FileInputStream(sourceFile);

            byte[] buffer = new byte[(int) sliceSize];
            int bytesRead;
            List<String> sliceFilePaths = new ArrayList<>();

            for (int i = 0; i < sliceNumber; i++) {
                int num = i + 1;
                shardingFileDTO.setShardIndex(num);

                String sliceFileName = "slice_" + num;
                String sliceFilePath = outputDirectory + File.separator + sliceFileName+"."+suffix;
                // 创建切片文件并写入数据
                FileOutputStream fos = new FileOutputStream(sliceFilePath);
                bytesRead = fis.read(buffer, 0, (int) sliceSize);
                fos.write(buffer, 0, bytesRead);
                fos.close();
                sliceFilePaths.add(sliceFilePath);

                File file = new File(sliceFilePath);
                excuteFile(shardingFileDTO,FileUtil.fileToMultipartFile(file));

            }
            fis.close();


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 合并成新的文件
     * @param filePaths
     * @param mergedFilePath
     */
    public static void mergeFiles(List<String> filePaths, String mergedFilePath) {
        try (FileOutputStream fos = new FileOutputStream(mergedFilePath);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            for (String filePath : filePaths) {
                try (FileInputStream fis = new FileInputStream(filePath);
                     BufferedInputStream bis = new BufferedInputStream(fis)) {
                    byte[] buffer = new byte[1024];
                    int bytesRead;
                    while ((bytesRead = bis.read(buffer)) != -1) {
                        bos.write(buffer, 0, bytesRead);
                    }
                }
            }
            log.info("文件合并完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 获取文件扩展名
     * @param fileName
     * @return
     */
    public static String getFileExtension(String fileName) {
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex > 0) {
            return fileName.substring(lastDotIndex + 1);
        }
        return ""; // 如果文件名中没有点,返回空字符串
    }

    /**
     * 删除文件夹下所有的文件
     * @param folderPath
     */
    public static void deleteFilesInFolder(String folderPath) {
        File folder = new File(folderPath);

        // 检查文件夹是否存在
        if (!folder.exists() || !folder.isDirectory()) {
            System.out.println("指定的路径不是一个有效的文件夹.");
            return;
        }

        File[] files = folder.listFiles();

        if (files != null) {
            for (File file : files) {
                if (file.isFile()) {
                    // 删除文件
                    if (file.delete()) {
                        System.out.println("已删除文件: " + file.getName());
                    } else {
                        System.out.println("无法删除文件: " + file.getName());
                    }
                }
            }
        }
    }

    /**
     * 删除文件夹
     * @param folderPath
     */
    public static void deleteFolder(String folderPath) {
        File folder = new File(folderPath);

        // 删除文件夹
        if (folder.exists() && folder.isDirectory()) {
            if (folder.delete()) {
                System.out.println("已删除文件夹: " + folderPath);
            } else {
                System.out.println("无法删除文件夹: " + folderPath);
            }
        }
    }

    /**
     * 删除文件夹中所有文件和子文件夹
     * @param folderPath
     */
    public static void deleteFolderAndSubfolders(String folderPath) {
        File folder = new File(folderPath);

        // 检查文件夹是否存在
        if (!folder.exists()) {
            System.out.println("文件夹不存在.");
            return;
        }

        if (folder.isDirectory()) {
            File[] files = folder.listFiles();

            if (files != null) {
                for (File file : files) {
                    if (file.isDirectory()) {
                        // 递归删除子文件夹及其内容
                        deleteFolderAndSubfolders(file.getAbsolutePath());
                    } else {
                        // 删除文件
                        if (file.delete()) {
                            System.out.println("已删除文件: " + file.getName());
                        } else {
                            System.out.println("无法删除文件: " + file.getName());
                        }
                    }
                }
            }
        }

        // 删除文件夹本身
        if (folder.delete()) {
            System.out.println("已删除文件夹: " + folderPath);
        } else {
            System.out.println("无法删除文件夹: " + folderPath);
        }
    }

}
class FilePathComparator implements Comparator<String> {
    private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+");

    @Override
    public int compare(String filePath1, String filePath2) {
        int number1 = extractNumber(filePath1);
        int number2 = extractNumber(filePath2);
        return Integer.compare(number1, number2);
    }

    private int extractNumber(String filePath) {
        Matcher matcher = NUMBER_PATTERN.matcher(filePath);
        if (matcher.find()) {
            return Integer.parseInt(matcher.group());
        }
        return 0; // 如果找不到数字,则返回0或其他适当的默认值
    }
}

3. 説明

バックエンド側でいちいちリクエストを呼び出すのは少々面倒なので、mainメソッドを使って説明します。

プログラムのスライス –> スライスの保存 (フロントエンドのアップロード) –> 最後のスライスのアップロード時にファイルのマージを実行 (条件付きインデックスに基づいてバックエンドのマージ) –> マージの完了 –> アップロード オブジェクト ストレージの実行 –> スライス ファイルの削除 –> インターフェース応答リンク

知らせ:

通常のインターフェースリクエストでは、excuteFile を少し変更する必要があり、スライスステップは省略できます。

4. FileUtil のメソッド

 /**
     * 创建文件夹
     *
     * @param path
     */
    public static void createFolder(String path) {
        File folder = new File(path);
        if (!folder.exists()) {
            folder.mkdirs();
        }
    }
    
    
     /**
     * 获取当前文件夹下面的文件列表
     *
     * @param folderPath
     * @return
     */
    public static List<String> listFiles(String folderPath) {
        List<String> objects = new ArrayList<>();
        File folder = new File(folderPath);
        File[] files = folder.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isFile()) {
                    objects.add(file.getAbsolutePath());
                } else if (file.isDirectory()) {
                    listFiles(file.getAbsolutePath());
                }
            }
        }
        return objects;
    }
    
      /**
     * File转换为MultipartFile
     * @param file
     * @return
     */
    public static MultipartFile fileToMultipartFile(File file) {
        FileItem item = new DiskFileItemFactory().createItem("file"
                , MediaType.MULTIPART_FORM_DATA_VALUE
                , true
                , file.getName());
        try (InputStream input = new FileInputStream(file);
             OutputStream os = item.getOutputStream()) {
            // 流转移
            IOUtils.copy(input, os);
        } catch (Exception e) {
            throw new IllegalArgumentException("Invalid file: " + e, e);
        }

        return new CommonsMultipartFile(item);
    }

参考: Java はファイルのアップロードを部分的に実装します

おすすめ

転載: blog.csdn.net/weixin_45285213/article/details/132662856