前言
我们的项目目前需要在一个相册中,上传多个的图片,因此,在一次的用户提交过程中,会有多张的图片需要被处理,那么此时,就需要有一个方法,来处理这多张文件。
很容易可以想到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(); } }