【Java项目】SpringBoot项目的多文件兼多线程上传下载

简介: 【Java项目】SpringBoot项目的多文件兼多线程上传下载

前言

我们的项目目前需要在一个相册中,上传多个的图片,因此,在一次的用户提交过程中,会有多张的图片需要被处理,那么此时,就需要有一个方法,来处理这多张文件。

很容易可以想到MultipartFile,我们在使用POST请求的时候就知道文件的单张上传都是POST请求加上一个@RequestParam的MultipartFile类型的文件。

如下

但是上面只能实现单张文件的上传,因此为了确保效率以及以及提交就能完成多文件的上传,需要把代码修改为如下状态,也就是请求参数为一个数组,这样子就能接受多文件的请求了

文件上传到本地代码编写

先最简单的介绍一下把文件保存到本地的代码编写

public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
            throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
            InvalidExtensionException
    {   //判断文件名长度是否过长
        int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
        if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
        {
            throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
        }
        //判断文件的扩展类型是否合理
        assertAllowed(file, allowedExtension);
        //对文件名进行编码
        String fileName = extractFilename(file);
        //获取文件在本机的绝对地址
        String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
        //将文件放到本机绝对地址
        file.transferTo(Paths.get(absPath));
        //返回文件名
        return getPathFileName(fileName);
    }
//比较重要的就是这个 他将会创建多级目录 并且把文件保存到对应的位置
private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException
    {
        File desc = new File(uploadDir + File.separator + fileName);
        if (!desc.exists())
        {
            if (!desc.getParentFile().exists())
            {
                desc.getParentFile().mkdirs();
            }
        }
        return desc.isAbsolute() ? desc : desc.getAbsoluteFile();
    }

文件上传之后,目录结构如下

其中文件目录结构指定位置为

全局线程池配置

我使用的SpringBoot版本为2.7.7,并且这是一个SpringCloud项目,因此,我对各种不同场景下的线程池进行了不同的配置,并且在需要使用到线程池的模块中直接映入这个线程池配置模块即可,代码如下,注意,这个代码是我自己编写的,很多类都是Java中没有的,你们按照自己的方法编写线程池配置即可

@AutoConfiguration
public class DynamicThreadPool {
    /**
     * 初始化线程池
     *
     * @return
     */
    @Bean("fileThreadPool")
    private static ThreadPoolExecutor buildThreadPoolExecutor() {
        return new ThreadPoolExecutor(3,
                3,
                60,
                TimeUnit.SECONDS,
                new ResizableCapacityLinkedBlockIngQueue<Runnable>(10),
                new NamedThreadFactory("file-thread-"),
                new ThreadPoolExecutor.DiscardPolicy());
    }
}

然后将这个类自动加载

实现多线程上传

其实实现多线程上传比较简单,很容易的就可以想到Thread类,ThreadPoolExecutor,Semaphore,CountdownLaunch,CompletableFuture,CyclicBarrier等各种解决方法。

这里先简单的列出CountDownLaunch配合ThreadPoolExecutor来实现多线程文件上传的方法

Semaphore

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。Semaphore 的使用简单,我们这里假设有 N(N>5) 个线程来获取 Semaphore 中的共享资源,下面的代码表示同一时刻 N 个线程中只有 5 个线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。

// 初始共享资源数量
final Semaphore semaphore = new Semaphore(5);
// 获取1个许可
semaphore.acquire();
// 释放1个许可
semaphore.release();
public List<String> uploadloadFileMultiples(MultipartFile[] files) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        Semaphore semaphore = new Semaphore(files.length);
        for (int i = 0; i < files.length; i++) {
            try {
                semaphore.acquire();
                MultipartFile file = files[i];
                //TODO 使用CountDownLaunch或者ComplatableFuture或者Semaphore
                //来完成多线程的文件上传
                fileThreadPool.submit(() -> {
                    try {
                        String url = uploadFile(file);
                        list.add(url);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        //表示一个文件已经被完成
                        semaphore.release();
                    }
                });
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return list;
    }

CountDownLaunch

CountDownLatch 有什么用?

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。

在这个项目中,我们要读取处理 3个文件,这 3个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。为此我们定义了一个线程池和 count 为3的CountDownLatch对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用CountDownLatch对象的 await()方法,直到所有文件读取完之后,才会接着执行后面的逻辑。

代码比较简单,也很好理解,也就是每一个文件拷贝完毕之后,调用countDownLaunch的countDown方法将计数器-1即可。

同时,由于是多线程拷贝,并且我需要保存每一次拷贝的返回结果,因此,就使用到了CopyOnWriteArrayList来保证并发情况下的数据集合不被丢失修改。

@Autowired
    @Qualifier("fileThreadPool")
    private ThreadPoolExecutor fileThreadPool;
    /**
     * 实现多文件多线程上传
     *
     * @param files 要上传的文件
     * @return 返回恋爱日志信息
     */
    @ApiOperation(value = "多附件上传-纯附件上传", notes = "多附件上传")
    @ResponseBody
    @PostMapping("/uploadFiles")
    public R<LoveLogs> handleFileUpload(@RequestParam("files") MultipartFile[] files) {
        LoveLogs loveLogs = new LoveLogs();
        String[] urls = new String[files.length];
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        CountDownLatch countDownLatch = new CountDownLatch(files.length);
        for (int i = 0; i < files.length; i++) {
            try {
                 获取文件名
                //String fileName = file.getOriginalFilename();
                 拼接文件保存路径
                //String filePath = "D:/uploads/" + fileName;
                 保存文件到本地
                //file.transferTo(new File(filePath));
                MultipartFile file = files[i];
                //String url = sysFileService.uploadFile(file);
                //urls[i] = url;
                //TODO 使用CountDownLaunch或者ComplatableFuture或者Semaphore
                //来完成多线程的文件上传
                fileThreadPool.submit(() -> {
                    try {
                        String s = sysFileService.uploadFile(file);
                        list.add(s);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        //表示一个文件已经被完成
                        countDownLatch.countDown();
                    }
                });
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        try {
            //阻塞直到所有的文件完成复制
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //统计每个文件的url
        String photoUrls = String.join(",", list);
        loveLogs.setUrls(photoUrls);
        //返回结果
        return R.ok(loveLogs);
    }

CyclicBarrier

CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CountDownLatch 的实现是基于 AQS 的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

CyclicBarrier 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。

//每次拦截的线程数
private final int parties;
//计数器
private int count;
• 1
• 2
• 3
• 4
public List<String> uploadloadFileMultipled(MultipartFile[] files) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        CyclicBarrier cyclicBarrier = new CyclicBarrier(files.length);
        for (int i = 0; i < files.length; i++) {
            try {
                MultipartFile file = files[i];
                //TODO 使用CountDownLaunch或者ComplatableFuture或者Semaphore
                //来完成多线程的文件上传
                fileThreadPool.submit(() -> {
                    try {
                        String url = uploadFile(file);
                        list.add(url);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        //表示一个文件已经被完成
                        try {
                            //进入await
                            cyclicBarrier.await(10,TimeUnit.SECONDS);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        } catch (BrokenBarrierException e) {
                            throw new RuntimeException(e);
                        } catch (TimeoutException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return list;
    }

实测

然后就是上传多个文件,并且发送请求了,下面是一个简单的请求模板,可以发现我上传完毕文件之后,他返回给我了本地的这个文件的位置,当然,这个文件的url地址啥的你们自己与前端对接完毕即可,我这里是多个url直接封装在一起并且使用英文逗号作为分隔符,具体情况自定义即可。

文件回显

文件的下载回显也比较简单,只要给出文件对应的位置,然后直接去本地加载即可。

这里文件回显暂时不做多线程优化,等后期项目需要了在做

/**
     * 文件下载
     *
     * @param name     文件名称
     * @param response 响应流
     */
    @GetMapping("/download")
    public void download(@RequestParam String name, HttpServletResponse response) {
        //FileInputStream fis = null;
        //ServletOutputStream os = null;
        try (FileInputStream fis = new FileInputStream(new File(name));
             ServletOutputStream os = response.getOutputStream()) {
            //输入流,通过输入流读取文件内容
            //fis = new FileInputStream(new File( name));
            //输出流,通过输出流将文件写回浏览器,在浏览器展示图片
            //os = response.getOutputStream();
            //设置响应的数据的格式
            response.setContentType("image/jpeg");
            int len = 0;
            byte[] buffre = new byte[1024 * 10];
            while ((len = fis.read(buffre)) != -1) {
                os.write(buffre, 0, len);
                os.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }




相关文章
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
488 0
|
10月前
|
Java Unix Go
【Java】(8)Stream流、文件File相关操作,IO的含义与运用
Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。!但本节讲述最基本的和流与 I/O 相关的功能。我们将通过一个个例子来学习这些功能。
367 1
|
监控 Java API
Java语言按文件创建日期排序及获取最新文件的技术
这段代码实现了文件创建时间的读取、文件列表的获取与排序以及获取最新文件的需求。它具备良好的效率和可读性,对于绝大多数处理文件属性相关的需求来说足够健壮。在实际应用中,根据具体情况,可能还需要进一步处理如访问权限不足、文件系统不支持某些属性等边界情况。
523 14
|
Java 数据库连接 API
Java 8 + 特性及 Spring Boot 与 Hibernate 等最新技术的实操内容详解
本内容涵盖Java 8+核心语法、Spring Boot与Hibernate实操,按考试考点分类整理,含技术详解与代码示例,助力掌握最新Java技术与应用。
364 2
|
Java 数据库连接 API
Java 对象模型现代化实践 基于 Spring Boot 与 MyBatis Plus 的实现方案深度解析
本文介绍了基于Spring Boot与MyBatis-Plus的Java对象模型现代化实践方案。采用Spring Boot 3.1.2作为基础框架,结合MyBatis-Plus 3.5.3.1进行数据访问层实现,使用Lombok简化PO对象,MapStruct处理对象转换。文章详细讲解了数据库设计、PO对象实现、DAO层构建、业务逻辑封装以及DTO/VO转换等核心环节,提供了一个完整的现代化Java对象模型实现案例。通过分层设计和对象转换,实现了业务逻辑与数据访问的解耦,提高了代码的可维护性和扩展性。
527 1
|
存储 Java 编译器
深入理解Java虚拟机--类文件结构
本内容介绍了Java虚拟机与Class文件的关系及其内部结构。Class文件是一种与语言无关的二进制格式,包含JVM指令集、符号表等信息。无论使用何种语言,只要能生成符合规范的Class文件,即可在JVM上运行。文章详细解析了Class文件的组成,包括魔数、版本号、常量池、访问标志、类索引、字段表、方法表和属性表等,并说明其在Java编译与运行过程中的作用。
362 0
|
存储 人工智能 Java
java之通过Http下载文件
本文介绍了使用Java实现通过文件链接下载文件到本地的方法,主要涉及URL、HttpURLConnection及输入输出流的操作。
808 0
|
Java 调度 流计算
基于Java 17 + Spring Boot 3.2 + Flink 1.18的智慧实验室管理系统核心代码
这是一套基于Java 17、Spring Boot 3.2和Flink 1.18开发的智慧实验室管理系统核心代码。系统涵盖多协议设备接入(支持OPC UA、MQTT等12种工业协议)、实时异常检测(Flink流处理引擎实现设备状态监控)、强化学习调度(Q-Learning算法优化资源分配)、三维可视化(JavaFX与WebGL渲染实验室空间)、微服务架构(Spring Cloud构建分布式体系)及数据湖建设(Spark构建实验室数据仓库)。实际应用中,该系统显著提升了设备调度效率(响应时间从46分钟降至9秒)、设备利用率(从41%提升至89%),并大幅减少实验准备时间和维护成本。
622 0
|
Java API 微服务
Java 21 与 Spring Boot 3.2 微服务开发从入门到精通实操指南
《Java 21与Spring Boot 3.2微服务开发实践》摘要: 本文基于Java 21和Spring Boot 3.2最新特性,通过完整代码示例展示了微服务开发全流程。主要内容包括:1) 使用Spring Initializr初始化项目,集成Web、JPA、H2等组件;2) 配置虚拟线程支持高并发;3) 采用记录类优化DTO设计;4) 实现JPA Repository与Stream API数据访问;5) 服务层整合虚拟线程异步处理和结构化并发;6) 构建RESTful API并使用Springdoc生成文档。文中特别演示了虚拟线程配置(@Async)和StructuredTaskSco
1304 0

热门文章

最新文章