场景

后台项目,功能是jxls模板导出表格,,表格模板已经,填充表格的图片都存在服务器上,导出是需要先下载到本地,当导出数据较多时,下载大量图片资源,单个下载会比较慢,优化下载资源较慢问题

导出表格示例:
网络资源下载慢 优化 - 图1

解决方案:开启多线程下载

  1. package com.ant.backstage.utils.file.multi;
  2. import org.apache.logging.log4j.LogManager;
  3. import org.apache.logging.log4j.Logger;
  4. import java.io.File;
  5. import java.io.IOException;
  6. import java.io.InputStream;
  7. import java.io.RandomAccessFile;
  8. import java.net.HttpURLConnection;
  9. import java.net.URL;
  10. public class MultiThreadDownload {
  11. private static Logger logger = LogManager.getLogger(MultiThreadDownload.class);
  12. private String str_url;
  13. private String storagePath;
  14. private int threadNumber;
  15. private static long downloadByteCount;
  16. public MultiThreadDownload(String str_url, String storagePath, int threadNumber) {
  17. this.str_url = str_url;
  18. this.storagePath = storagePath;
  19. this.threadNumber = threadNumber;
  20. }
  21. public void download() throws IOException, InterruptedException {
  22. long startTime = System.currentTimeMillis();
  23. logger.info("Download[多线程下载资源开始]......");
  24. /*
  25. * 首先设置本地文件的大小
  26. * 当然这是个null数据的文件
  27. * 这样才能通过RandomAccessFile的数组下标机制达到随机位置写入
  28. */
  29. URL url = new URL(str_url);
  30. HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  31. conn.setConnectTimeout(10000);
  32. conn.setRequestMethod("GET");
  33. long fileLength = conn.getContentLengthLong(); // 得到需要下载的文件大小
  34. conn.disconnect();
  35. RandomAccessFile file = new RandomAccessFile(storagePath, "rwd");
  36. /**
  37. * 在RandomAccessFile的构造方法中,第二个参数是用来这是访问文件模式的,分为四种
  38. *
  39. * “r” 以只读方式打开文件
  40. * “rw” 以读写方式打开文件
  41. * “rws” 以读写方式打开文件 ,相当于rw模式,还要求对文件的内容或者元数据的每个更新都同步写入底层存储设备
  42. * “rwd” 以读写方式打开文件 ,相当于rw模式,还要求对文件的内容每个更新都同步写入底层存储设备
  43. **/
  44. file.setLength(fileLength+1); // 关键方法 : 设置本地文件长度
  45. file.close();
  46. /**
  47. * 问题1,java.io.FileNotFoundException: D:\idea-develop-project\Project_All\backstage_ant\web\src\main\resources\jxls_images (拒绝访问。)
  48. *
  49. * 原因: 在进行分析时,我得说清楚什么时候抛拒绝访问,什么时候抛找不到指定路径。原因是这样的,在构造一个File对象时,
  50. * 指定的文件路径是什么都可以,就算不存在也能够构造File对象,但是,
  51. * 现在你要对文件进行输入输出操作,也就是InputStream和OutputStream操作时,
  52. * 如果填写的路径不存在,那么就会报系统找不到指定路径,如果指定的是目录时,就会报拒绝访问异常。看了这个前提之后,在继续往下读。
  53. * https://www.cnblogs.com/zhuyeshen/p/11435879.html
  54. *
  55. *
  56. * 问题2,异常 试图将文件指针移到文件开头之前。
  57. *
  58. * 解决:【改变本地文件大小】
  59. * 如果文件串的最后一个字符是中文,使用mark()中的长度设为file.length()
  60. * 如果文件的最后一个字符串是英文或数字,则java.io.IOException: Mark invalid,使用mark()中的长度设为file.length()+1
  61. *
  62. * 问题3,java.net.MalformedURLException: no protocol:
  63. *
  64. * 原因:no protocol,没有指定通信协议异常。【没有指定 http 协议,在 URL 前面加上http://即可解决此异常。】
  65. *
  66. **/
  67. /*
  68. * 计算每条线程下载的字节数,以及每条线程起始下载位置与结束的下载位置,
  69. * 因为不一定平均分,所以最后一条线程下载剩余的字节
  70. * 然后创建线程任务并启动
  71. * Main线程等待每条线程结束(join()方法)
  72. */
  73. long oneThreadReadByteLength = fileLength / threadNumber;
  74. for (int i = 0; i < threadNumber; i++) {
  75. long startPosition = i * oneThreadReadByteLength;
  76. long endPosition = i == threadNumber - 1 ? fileLength : (i + 1) * oneThreadReadByteLength - 1;
  77. Thread t = new Thread(new Task(startPosition, endPosition));
  78. t.start();
  79. t.join();
  80. }
  81. /*
  82. * 检查文件是否下载完整,不完整则删除
  83. */
  84. if (downloadByteCount == fileLength) {
  85. logger.info("ALL Thread Download OK.【资源下载完成】");
  86. logger.info("【资源下载完成,耗时】time = " + ((System.currentTimeMillis() - startTime)) + "ms");
  87. } else {
  88. logger.error("Download Error.【资源下载异常,移除异常资源】");
  89. new File(storagePath).delete();
  90. }
  91. }
  92. class Task implements Runnable {
  93. private long startPosition;
  94. private long endPosition;
  95. Task(long startPosition, long endPosition) {
  96. this.startPosition = startPosition;
  97. this.endPosition = endPosition;
  98. }
  99. @Override
  100. public void run() {
  101. try {
  102. URL url = new URL(str_url);
  103. HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  104. conn.setConnectTimeout(10000);
  105. conn.setRequestMethod("GET");
  106. conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition); // 关键方法: 每条线程请求的字节范围
  107. if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) { // 关键响应码 :206,请求成功 + 请求数据字节范围成功
  108. RandomAccessFile file = new RandomAccessFile(storagePath, "rwd");
  109. file.seek(startPosition); // 关键方法 :每条线程起始写入文件的位置
  110. InputStream in = conn.getInputStream();
  111. byte[] buf = new byte[8192];
  112. int len;
  113. while ((len = in.read(buf)) > 0) {
  114. file.write(buf, 0, len);
  115. downloadByteCount += len;
  116. }
  117. // 关闭网络连接及本地流
  118. in.close();
  119. file.close();
  120. conn.disconnect();
  121. logger.info(Thread.currentThread().getName() + ": download OK");
  122. }
  123. } catch (IOException e) {
  124. logger.error(Thread.currentThread().getName() + "_Error : " + e);
  125. }
  126. }
  127. }
  128. public static void main(String[] args) throws Exception {
  129. //测试使用网络图片可以实现多线程下载,由于我把gitee当成服务器使用,但是gitee无法使用多线程下载一个文件
  130. // String path = "https://gitee.com/Sir-yuChen/backstage_ant_upload/raw/master/file/upload_img/1642933041448_c33d4a8e-2dd5-4d49-82eb-61fb147811b5.jpg";
  131. String path = "https://tse4-mm.cn.bing.net/th/id/OIP-C.vpLlKLk2137OWLAF2oMzZAHaE8?w=269&h=180&c=7&r=0&o=5&pid=1.7";
  132. MultiThreadDownload mtd = new MultiThreadDownload(path, "D:\\idea-develop-project\\Project_All\\backstage_ant\\web\\src\\main\\resources\\jxls_images\\LeiMus.jpg", 10);
  133. mtd.download();
  134. }
  135. }