一、服务端/客户端代码的实现
服务端配置config
1 @ConfigurationProperties("storage")2 public classStorageProperties {3 private String location = "D:\\idea_project\\upload\\src\\main\\resources\\upload-files";4
5 publicString getLocation() {6 returnlocation;7 }8
9 public voidsetLocation(String location) {10 this.location =location;11 }12 }
服务端Controller
1 @GetMapping("/files/{filename:.+}")2 @ResponseBody3 public ResponseEntityserveFile(@PathVariable String filename) {4 Resource file =storageService.loadAsResource(filename);5 returnResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,6 "attachment; filename=\"" + file.getFilename() + "\"").body(file);7 }
服务端Service
1 Path load(String filename);2
3 Resource loadAsResource(String filename);
1 packageorg.wlgzs.upload.service.impl;2
3 importorg.springframework.beans.factory.annotation.Autowired;4 importorg.springframework.core.io.Resource;5 importorg.springframework.core.io.UrlResource;6 importorg.springframework.stereotype.Service;7 importorg.springframework.util.FileSystemUtils;8 importorg.springframework.util.StringUtils;9 importorg.springframework.web.multipart.MultipartFile;10 importorg.wlgzs.upload.config.StorageProperties;11 importorg.wlgzs.upload.service.StorageService;12
13 importjava.io.IOException;14 importjava.net.MalformedURLException;15 importjava.nio.file.Files;16 importjava.nio.file.Path;17 importjava.nio.file.Paths;18 importjava.nio.file.StandardCopyOption;19 importjava.util.stream.Stream;20
21 /**
22 *@authorzsh23 * @company wlgzs24 * @create 2018-12-15 16:1625 * @Describe26 */
27
28 @Service29 public class FileSystemStorageService implementsStorageService {30
31 private finalPath rootLocation;32
33 @Autowired34 publicFileSystemStorageService(StorageProperties properties) {35 this.rootLocation =Paths.get(properties.getLocation());36 }37
38 @Override39 publicPath load(String filename) {40 returnrootLocation.resolve(filename);41 }42
43 @Override44 publicResource loadAsResource(String filename) {45 try{46 Path file =load(filename);47 Resource resource = newUrlResource(file.toUri());48 if (resource.exists() ||resource.isReadable()) {49 returnresource;50 }51 else{52 System.out.println("Could not read file: " +filename);53 //throw new StorageFileNotFoundException("Could not read file: " + filename);
54
55 }56 }57 catch(MalformedURLException e) {58 System.out.println("Could not read file: " +filename);59 //throw new StorageFileNotFoundException("Could not read file: " + filename, e);
60 }61 return null;62 }63
64 }
服务端目录结构
客户端Main类
1 importjava.util.Scanner;2 importjava.util.concurrent.TimeUnit;3
4 /**
5 *@authorzsh6 * @site www.qqzsh.top7 * @company wlgzs8 * @create 2019-05-27 9:039 * @description 主线程启动入口10 */
11 public classMain {12 public static voidmain(String[] args) {13 Scanner scanner = newScanner(System.in);14 System.out.println("请输入下载文件的地址,按ENTER结束");15 String downpath =scanner.nextLine();16 System.out.println("下载的文件名及路径为:"+MultiPartDownLoad.downLoad(downpath));17 try{18 System.out.println("下载完成,本窗口5s之后自动关闭");19 TimeUnit.SECONDS.sleep(5);20 } catch(InterruptedException e) {21 e.printStackTrace();22 }23 System.exit(0);24 }25 }
客户端线程池Constans类
1 import java.util.concurrent.*;2
3 /**
4 *@authorzsh5 * @site www.qqzsh.top6 * @company wlgzs7 * @create 2019-05-27 8:528 * @description 自定义线程池9 */
10 public classConstans {11
12 public static final int MAX_THREAD_COUNT =getSystemProcessCount();13 private static final int MAX_IMUMPOOLSIZE =MAX_THREAD_COUNT;14
15 /**
16 * 自定义线程池17 */
18 private staticExecutorService MY_THREAD_POOL;19 /**
20 * 自定义线程池21 */
22 public staticExecutorService getMyThreadPool(){23 if(MY_THREAD_POOL == null){24 MY_THREAD_POOL =Executors.newFixedThreadPool(MAX_IMUMPOOLSIZE);25 }26 returnMY_THREAD_POOL;27 }28
29 /**
30 * 线程池31 */
32 private staticThreadPoolExecutor threadPool;33
34 /**
35 * 单例,单任务 线程池36 *@return
37 */
38 public staticThreadPoolExecutor getThreadPool(){39 if(threadPool == null){40 threadPool = new ThreadPoolExecutor(MAX_IMUMPOOLSIZE, MAX_IMUMPOOLSIZE, 3, TimeUnit.SECONDS,41 new ArrayBlockingQueue<>(16),42 newThreadPoolExecutor.CallerRunsPolicy()43 );44 }45 returnthreadPool;46 }47
48 /**
49 * 获取服务器cpu核数50 *@return
51 */
52 private static intgetSystemProcessCount(){53 returnRuntime.getRuntime().availableProcessors();54 }55 }
客户端多线程下载类MultiPartDownLoad
1 importjava.io.File;2 importjava.io.IOException;3 importjava.io.InputStream;4 importjava.io.RandomAccessFile;5 importjava.net.HttpURLConnection;6 importjava.net.URL;7 importjava.util.UUID;8 importjava.util.concurrent.CountDownLatch;9 importjava.util.concurrent.ExecutorService;10 importjava.util.concurrent.locks.ReentrantLock;11
12 /**
13 *@authorzsh14 * @site www.qqzsh.top15 * @company wlgzs16 * @create 2019-05-27 8:5317 * @description 多线程下载主程序18 */
19 public classMultiPartDownLoad {20 /**
21 * 线程下载成功标志22 */
23 private static int flag = 0;24
25 /**
26 * 服务器请求路径27 */
28 privateString serverPath;29 /**
30 * 本地路径31 */
32 privateString localPath;33 /**
34 * 线程计数同步辅助35 */
36 privateCountDownLatch latch;37 /**
38 * 定长线程池39 */
40 private staticExecutorService threadPool;41
42 publicMultiPartDownLoad(String serverPath, String localPath) {43 this.serverPath =serverPath;44 this.localPath =localPath;45 }46
47 public booleanexecuteDownLoad() {48 try{49 URL url = newURL(serverPath);50 HttpURLConnection conn =(HttpURLConnection) url.openConnection();51 //设置超时时间
52 conn.setConnectTimeout(5000);53 //设置请求方式
54 conn.setRequestMethod("GET");55 conn.setRequestProperty("Connection", "Keep-Alive");56 int code =conn.getResponseCode();57 if (code != 200) {58 System.out.println(String.format("无效网络地址:%s", serverPath));59 return false;60 }61 //服务器返回的数据的长度,实际上就是文件的长度,单位是字节62 //int length = conn.getContentLength();//文件超过2G会有问题
63 long length =getRemoteFileSize(serverPath);64
65 System.out.println("远程文件总长度:" + length + "字节(B),"+length/Math.pow(2,20)+"MB");66 RandomAccessFile raf = new RandomAccessFile(localPath, "rwd");67 //指定创建的文件的长度
68 raf.setLength(length);69 raf.close();70 //分割文件
71 int partCount =Constans.MAX_THREAD_COUNT;72 int partSize = (int)(length /partCount);73 latch = newCountDownLatch(partCount);74 threadPool =Constans.getMyThreadPool();75 for (int threadId = 1; threadId <= partCount; threadId++) {76 //每一个线程下载的开始位置
77 long startIndex = (threadId - 1) *partSize;78 //每一个线程下载的开始位置
79 long endIndex = startIndex + partSize - 1;80 if (threadId ==partCount) {81 //最后一个线程下载的长度稍微长一点
82 endIndex =length;83 }84 System.out.println("线程" + threadId + "下载:" + startIndex + "字节~" + endIndex + "字节");85 threadPool.execute(newDownLoadThread(threadId, startIndex, endIndex, latch));86 }87 latch.await();88 if(flag == 0){89 return true;90 }91 } catch(Exception e) {92 System.out.println(String.format("文件下载失败,文件地址:%s,失败原因:%s", serverPath, e.getMessage()));93 }94 return false;95 }96
97 /**
98 * 内部类用于实现下载99 */
100 public class DownLoadThread implementsRunnable {101
102 /**
103 * 线程ID104 */
105 private intthreadId;106 /**
107 * 下载起始位置108 */
109 private longstartIndex;110 /**
111 * 下载结束位置112 */
113 private longendIndex;114
115 privateCountDownLatch latch;116
117 DownLoadThread(int threadId, long startIndex, longendIndex, CountDownLatch latch) {118 this.threadId =threadId;119 this.startIndex =startIndex;120 this.endIndex =endIndex;121 this.latch =latch;122 }123
124 @Override125 public voidrun() {126 try{127 System.out.println("线程" + threadId + "正在下载...");128 URL url = newURL(serverPath);129 HttpURLConnection conn =(HttpURLConnection) url.openConnection();130 conn.setRequestProperty("Connection", "Keep-Alive");131 conn.setRequestMethod("GET");132 //请求服务器下载部分的文件的指定位置
133 conn.setRequestProperty("Range", "bytes=" + startIndex + "-" +endIndex);134 conn.setConnectTimeout(5000);135 int code =conn.getResponseCode();136 System.out.println("线程" + threadId + "请求返回code=" +code);137 //返回资源
138 InputStream is =conn.getInputStream();139 RandomAccessFile raf = new RandomAccessFile(localPath, "rwd");140 //随机写文件的时候从哪个位置开始写141 //定位文件
142 raf.seek(startIndex);143 intlen;144 byte[] buffer = new byte[1024];145 int realLen = 0;146 while ((len = is.read(buffer)) != -1) {147 realLen +=len;148 raf.write(buffer, 0, len);149 }150 System.out.println("线程" + threadId + "下载文件大小=" + realLen/Math.pow(2,20)+"MB");151 is.close();152 raf.close();153 System.out.println("线程" + threadId + "下载完毕");154 } catch(Exception e) {155 //线程下载出错
156 MultiPartDownLoad.flag = 1;157 System.out.println(e.getMessage());158 } finally{159 //计数值减一
160 latch.countDown();161 }162 }163 }164
165 /**
166 * 内部方法,获取远程文件大小167 *@paramremoteFileUrl168 *@return
169 *@throwsIOException170 */
171 private long getRemoteFileSize(String remoteFileUrl) throwsIOException {172 longfileSize;173 HttpURLConnection httpConnection = (HttpURLConnection) newURL(remoteFileUrl).openConnection();174 httpConnection.setRequestMethod("HEAD");175 int responseCode = 0;176 try{177 responseCode =httpConnection.getResponseCode();178 } catch(IOException e) {179 e.printStackTrace();180 }181 if (responseCode >= 400) {182 System.out.println("Web服务器响应错误!请稍后重试");183 return 0;184 }185 String sHeader;186 for (int i = 1;; i++) {187 sHeader =httpConnection.getHeaderFieldKey(i);188 if ("Content-Length".equals(sHeader)) {189 fileSize =Long.parseLong(httpConnection.getHeaderField(sHeader));190 break;191 }192 }193 returnfileSize;194 }195
196 /**
197 * 下载文件执行器198 *@paramserverPath199 *@return
200 */
201 public synchronized staticString downLoad(String serverPath) {202 ReentrantLock lock = newReentrantLock();203 lock.lock();204
205 String[] names = serverPath.split("\\.");206 if (names.length <= 0) {207 return null;208 }209 String fileTypeName = names[names.length - 1];210 String localPath = String.format("%s.%s", new File("").getAbsolutePath()+"\\"+UUID.randomUUID(),fileTypeName);211 MultiPartDownLoad m = newMultiPartDownLoad(serverPath, localPath);212 long startTime =System.currentTimeMillis();213 boolean flag = false;214 try{215 flag =m.executeDownLoad();216 long endTime =System.currentTimeMillis();217 if(flag){218 System.out.println("文件下载结束,共耗时" + (endTime - startTime)+ "ms");219 returnlocalPath;220 }221 System.out.println("文件下载失败");222 return null;223 }catch(Exception ex){224 System.out.println(ex.getMessage());225 return null;226 }finally{227 //重置 下载状态
228 MultiPartDownLoad.flag = 0;229 if(!flag){230 File file = newFile(localPath);231 file.delete();232 }233 lock.unlock();234 }235 }236 }
客户端目录结构
下载效果:
二、核心部分
多线程下载不仅需要客户端的支持,也需要服务端的支持。 conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);Range响应头是多线程下载分割的关键所在。
下载思路:首先判断下载文件大小,配合多线程分割定制http请求数量和请求内容,响应到写入到RandomAccessFile指定位置中。在俗点就是大的http分割成一个个小的http请求,相当于每次请求一个网页。RandomAccessFile文件随机类,可以向文件写入指定位置的流信息。
三、将Java类打包成jar(idea)
1、创建空jar
2、将.class文件加入jar
此时要注意,如果类存在包名,需要一级一级建立与之对应的包名
3、创建Manifest
Manifest-Version: 1.0Main-Class: Main
4、build jar包
5、如果出现找不到或无法加载主类,就看下Main-Class是否为完整包名。
四、在无Java环境的win上执行bat
目录
bat脚本
start jre\bin\java -jar download.jar
五、完整程序地址
链接: https://pan.baidu.com/s/1Yy9fnRut9gLo5ENgtoElpA 提取码: mh3c 复制这段内容后打开百度网盘手机App,操作更方便哦