后文有源代码的仓库地址,需要的自取,给个点赞,不过分吧
本项目代码是我照着敲的,加了一些代码解释,之前还是学了一些多线程的知识,一直没有运用,有时间的话可以去支持一下原作者,喜欢本文的话可以点赞+收藏+分享
开发工具:IDEA
Java版本:jdk8
项目编码:UTF-8
Java项目:普通的JavaSE项目
项目结构:
constant:存放常量类
core:下载器核心类包
util:存放工具类
Main:主类
从互联网下载文件有点类似于我们将本地某个文件复制到另一个目录下,也会利用 IO 流
进行操作。对于从互联网下载,还需要将本地和下载文件所在的服务器建立连接。
计算机从服务器下载文件的过程可以分为以下几个步骤:
创建工具类com.util.Httputils
package com.util;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* @Author YZK
* @Date 2023/10/18
*/
public class HttpUtils {
/***
* 获取HttpURLConnection链接对象
* @param url 文件地址
* @return HttpURLConnection
*/
public static HttpURLConnection getHttpURLConnection(String url) throws IOException {
URL httpURL = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection) httpURL.openConnection();
//向文件所在的服务器发达标识信息
httpURLConnection.setRequestProperty("User-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.46");
return httpURLConnection;
}
/***
* 获取文件名
* @param url 文件地址
* @return 文件名
*/
public static String getFileName(String url) {
//找到URL字符串中最后一个斜杠(/)的位置,将其索引值赋给整型变量index。
int index = url.lastIndexOf("/");
//从index+1的位置开始截取字符串,即从最后一个斜杠之后的部分开始截取,直到字符串末尾
return url.substring(index + 1);
}
}
编写核心下载器com.core.Downloader
package com.core;
import com.constant.Constant;
import com.util.HttpUtils;
import java.io.*;
import java.net.HttpURLConnection;
/**
* @Author YZK
* @Date 2023/10/18
* 下载器核心类
*/
public class Downloader {
public void download(String url) {
//获取文件名
String httpFileName = HttpUtils.getFileName(url);
//拼接文件下载路径
httpFileName = Constant.PATH + httpFileName;
HttpURLConnection httpURLConnection = null;
try {
//获取链接对象
httpURLConnection = HttpUtils.getHttpURLConnection(url);
} catch (IOException e) {
throw new RuntimeException(e);
}
try (
//创建输入流
InputStream inputStream = httpURLConnection.getInputStream();
//创建缓冲输入流
BufferedInputStream bis = new BufferedInputStream(inputStream);
//创建输出流
FileOutputStream fos = new FileOutputStream(httpFileName);
//创建缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(fos)
) {
int len = -1;
while ((len = bis.read()) != -1) {
bos.write(len);
}
} catch (FileNotFoundException fileNotFoundException) {
System.out.println("文件不存在");
} catch (Exception e) {
System.out.println("下载失败");
} finally {
httpURLConnection.disconnect();
}
}
}
给len的初始值为-1是因为,InputStream.read()方法返回一个整数,表示读取到的字节的值。如果已经到达流的末尾,则该方法返回-1。
InputStream.read()源代码
检查当前位置(pos)是否大于等于缓冲区中的字节数(count)。如果是,则执行下一行代码。fill()这个方法会从输入流中读取更多的数据并将其存储在缓冲区中,然后再次检查位置(pos)是否大于等于缓冲区中的字节数(count)。如果是,则返回-1,表示已经到达输入流的末尾。
日志工具类com.util.LogUtils
package com.util;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* @Author YZK
* @Date 2023/10/19
*/
public class LogUtils {
public static void info(String msg, Object... args) {
print(msg, "-info-", args);
}
public static void error(String msg, Object... args) {
print(msg, "-error-", args);
}
private static void print(String msg, String level, Object... args) {
if (args != null && args.length > 0) {
msg = String.format(msg.replace("{}", "%s"), args);
}
String name = Thread.currentThread().getName();
System.out.println(LocalTime.now().format(DateTimeFormatter.ofPattern("hh:mm:ss")) + " " + name + level + msg);
}
}
文件工具类com.util.FileUtils用于针对文件相关操作,获取文件大小,比较文件MD5
package com.util;
import com.constant.Constant;
import com.sun.org.glassfish.gmbal.ManagedData;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @Author YZK
* @Date 2023/10/19
*/
public class FileUtils {
/***
* 获取本地文件的大小
* @param path 文件路径
* @return 文件大小
*/
public static long getFileContentLength(String path) {
File file = new File(path);
return file.exists() && file.isFile() ? file.length() : 0;
}
/***
* 获取文件MD5值
* @param filePath 文件路径
* @return boolean
*/
public static String getFileMD5(String filePath) throws IOException, NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
FileInputStream fis = new FileInputStream(filePath);
byte[] buffer = new byte[Constant.BYTE_SIZE];
int length;
while ((length = fis.read(buffer)) != -1) {
md.update(buffer, 0, length);
}
fis.close();
byte[] digest = md.digest();
BigInteger bigInt = new BigInteger(1, digest);
return bigInt.toString(16);
}
/***
* 比较文件MD5
* @param filePath1 文件1的路径
* @param filePath2 文件2的路径
* @return boolean
*/
public static boolean verifyFileMD5(String filePath1, String filePath2) throws IOException, NoSuchAlgorithmException {
String file1MD5 = getFileMD5(filePath1);
String file2MD5 = getFileMD5(filePath2);
return file1MD5.equals(file2MD5);
}
}
Http请求工具类com.util.HttpUtils用于处理http请求
package com.util;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* @Author YZK
* @Date 2023/10/18
*/
public class HttpUtils {
/***
* 获取HttpURLConnection链接对象
* @param url 文件地址
* @return HttpURLConnection
*/
public static HttpURLConnection getHttpURLConnection(String url) throws IOException {
URL httpURL = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection) httpURL.openConnection();
//向文件所在的服务器发达标识信息
httpURLConnection.setRequestProperty("User-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.46");
return httpURLConnection;
}
/***
* 获取文件名
* @param url 文件地址
* @return 文件名
*/
public static String getFileName(String url) {
//找到URL字符串中最后一个斜杠(/)的位置,将其索引值赋给整型变量index。
int index = url.lastIndexOf("/");
//从index+1的位置开始截取字符串,即从最后一个斜杠之后的部分开始截取,直到字符串末尾
return url.substring(index + 1);
}
}
文件下载的时候最好能够展示出下载的速度,已下载文件大小等信息。这里可以每隔一段时间来获取文件的下载信息,比如间隔 1 秒获取一次,然后将信息打印到控制台。文件下载是个独立的线程,另外还需要再开启一个线程来间隔获取文件的信息。java,util.concurrent.ScheduledExecutorService,这个类可以帮助我们来实现此功能。
package com.core;
import com.constant.Constant;
/**
* @Author YZK
* @Date 2023/10/19
* 展示下载信息
*/
public class DownloadInfoThread implements Runnable {
//下载文件总大小
private long httpFileContentLength;
//本地已经下载文件的大小
public double finishedSize;
//本次累计下载的大小
public volatile double downSize;
//前一次下载的大小
public double prevSize;
public DownloadInfoThread(long httpFileContentLength) {
this.httpFileContentLength = httpFileContentLength;
}
@Override
public void run() {
//计算文件总大小,mb
String httpFileSize = String.format("%.2f", httpFileContentLength / Constant.MB);
//计算每秒下载速度,kb
int speed = (int) ((downSize - prevSize) / 1024d);
prevSize = downSize;
//计算剩余文件大小
double remainSize = httpFileContentLength - finishedSize - downSize;
//计算剩余时间
String remainTime = String.format("%.1f", remainSize / 1024d / speed);
if ("Infinity".equalsIgnoreCase(remainTime)) {
remainTime = "-";
}
//已下载文件大小
String currentFileSize = String.format("%.2f", (downSize - finishedSize) / Constant.MB);
String downInfo = String.format("已下载%smb/%smb,速度%skb/s,剩余时间%ss", currentFileSize, httpFileSize, speed, remainTime);
System.out.print("\r");
System.out.print(downInfo);
}
}
本节与下载器关系不大,只是本人学习时的记录,可跳过
线程池,顾名思义,是一种用来存放和管理线程的机制。它的主要实现思想是基于池化技术,预先创建一定数量的线程并将它们存储在一个“池子”内。当有新的任务需要处理时,线程池内部会复用这些预先创建的线程来执行任务,避免了频繁地创建和销毁线程所带来的性能开销。
在具体的工作过程中,当一个并发任务提交给线程池时,线程池会进行一系列的判断与分配。首先,它会判断核心线程池中的所有线程是否都在执行任务,如果不是,就会新创建一个线程来执行刚提交的任务;如果核心线程池中所有的线程都在执行任务,那么新提交的任务就会被放置在阻塞队列中等待。
我们为什么需要使用线程池呢?首先,线程的创建和销毁是要占用一定的资源的。具体来说,创建线程会直接向系统申请资源,包括分配内存、列入调度等,同时线程还要进行上下文的切换。再者,对于Java来说,每个线程都需要一定的内存来存储上下文信息,并且这个内存是在Java堆外,不受我们的程序控制,只受系统资源限制。因此,如果我们为每个请求都新建一个线程,那么系统的内存资源很快就会被耗尽。通过使用线程池,我们可以降低频繁创建和销毁线程所带来的资源消耗,提高系统的性能和稳定性。
线程池的工作过程主要包括以下几个步骤:
线程池具有五种主要状态,分别是:Running、ShutDown、Stop、Tidying和Terminated。
RUNNING:这是线程池的正常运行状态。在此状态下,线程池能够接收新任务,并对已添加的任务进行处理。
SHUTDOWN:当线程池进入此状态时,它不再接收新任务。但是,所有已提交的任务将被执行完毕,同时线程池中已存在的线程将继续执行完它们的任务。
STOP:与SHUTDOWN状态不同,当线程池处于STOP状态时,不仅不会接收新任务,而且会停止所有已存在的线程。
TIDYING:在线程池关闭的过程中,线程池首先会进入Tidying状态。在此状态下,所有已接收但还未处理的任务都会被丢弃。
TERMINATED:这是线程池的最终状态。当线程池中所有的任务都执行完毕后,线程池将进入此状态。
线程池的关闭主要涉及到shutdown和shutdownNow两种方法。shutdown方法是用于拒绝新任务提交并等待已提交的任务执行完毕后再关闭线程池,这样可以避免正在执行的任务被突然中断。而shutdownNow方法则是用来立即停止接收新任务,并且尝试停止所有正在执行的任务,这种方式相对强硬,可能会导致正在执行的任务被中断。
在具体使用时,需要根据实际需求来选择使用哪种关闭方式。比如,如果希望尽快关闭线程池并且不关心已提交任务的执行情况,可以选择使用shutdownNow方法。反之,如果希望尽可能完成已提交任务的执行,然后再关闭线程池,那么shutdown方法是更合适的选择。
此外,需要注意的是,线程池的关闭过程是一个温和且安全的过程。在实际应用中,我们需要确保在适当的时候调用这些关闭方法,避免因为不正确的使用导致线程池无法正常关闭或者出现其他问题。
java.util.concurrent.ThreadPooTExecutor类构造方法参数详解
corePoolSize:线程池中核心线程的数量
maximumPoolSize:线程池中最大线程数,是核心线程数和非核心线程数之和
keepAliveTime:非核心线程存活时间(非核心线程会被销毁,核心线程不会)
unit:存活时间单位
workQueue:当没有空闲的线程时,新的任务会加入到 workQueue 中排队等待
threadFactory:线程工厂,用于创造线程
handler:当线程池中的线程数量已经达到最大值,且队列中的任务数量也达到了最大值时,新提交的任务就会被拒绝执行。此时,如果handler不为空,就会调用它的rejectedExecution方法来处理被拒绝的任务。
public class PoolTest {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 3, 1, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(2));
Runnable r = () -> System.out.println(Thread.currentThread().getName());
threadPool.execute(r);
}
}
//运行结果
//pool-1-thread-1
Executors 是 Java 中的一个工厂类,专门用于创建和管理线程池。它提供了一系列静态工厂方法,能够创建不同类型的线程池,如 newCachedThreadPool()、newFixedThreadPool()、newSingleThreadExecutor() 和 newScheduledThreadPool() 等。这些工厂方法极大地简化了线程池的创建过程,隐藏了线程池的复杂性。
以下是 Executors 提供的几种主要线程池类型:
总的来说,Executors 提供了一种简单有效的方式来创建和管理线程池,是 Java 多线程编程中的重要工具类。
//这是一个固定线程池,它会预先创建5个线程并保持这些线程的可用状态。当提交任务时,如果有空闲线程,任务将立即执行;如果没有空闲线程,任务将在等待队列中等待,直到有线程可用。
ExecutorService executorService1 = Executors.newFixedThreadPool(5);
//这是一个可缓存线程池,它可以缓存最多127个线程。当提交任务时,如果有空闲线程,任务将立即执行;如果没有空闲线程,线程池将创建一个新线程来执行任务。当线程空闲超过60秒时,它们将被终止并从缓存中移除。
ExecutorService executorService2 = Executors.newCachedThreadPool();
//这是一个单线程执行器,它只使用一个线程来执行任务。这意味着提交的任务将按顺序执行,而不是并发执行。
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
//这是一个定时线程池,它可以定时或周期性地执行任务。在这个例子中,它创建了一个包含5个线程的线程池,可以用来执行定时任务。
ScheduledExecutorService executorService4 = Executors.newScheduledThreadPool(5);
其内部实现,也是使用了上一节写到的java.util.concurrent.ThreadPooTExecutor类;但是!!!阿里巴巴开发文档中并不建议使用以上4中方式创建线程池,建议还是使用原生的创建方式
在com.util.HttpUtils中添加以下方法
/***
*分块下载
* @param url 文件地址
* @param startPos 下载文件起始位置
* @param endPos 下载文件结束位置
* @return HttpURLConnection
*/
public static HttpURLConnection getHttpURLConnection(String url, long startPos, long endPos) throws IOException {
HttpURLConnection httpURLConnection = getHttpURLConnection(url);
LogUtils.info("下载的区间是:{}-{}", startPos, endPos);
if (endPos != 0) {
httpURLConnection.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);
}else {
httpURLConnection.setRequestProperty("Range", "bytes=" + startPos);
}
return httpURLConnection;
}
代码中判断endPos是否不等于0。这段代码是用于设置HTTP请求头的"Range"属性,以便在下载文件时指定要获取的字节范围。
endPos
是否不等于0,如果不等于0,说明需要指定一个具体的字节范围;endPos
等于0,说明只需要指定起始字节位置startPos
。endPos
不等于0)或"bytes=" + startPos(如果endPos
等于0)。这样设置后,服务器会返回从startPos
到endPos
之间的字节数据。
com.core.DownloadTask
package com.core;
import com.constant.Constant;
import com.util.HttpUtils;
import com.util.LogUtils;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.util.concurrent.Callable;
/**
* @Author YZK
* @Date 2023/10/25
*/
public class DownloadTask implements Callable<Boolean> {
private String url;
//下载的起始位置
private long startPos;
//下载的结束位置
private long endPos;
//标识当前文件是第几部分
private int part;
public DownloadTask(String url, long startPos, long endPos, int part) {
this.url = url;
this.startPos = startPos;
this.endPos = endPos;
this.part = part;
}
@Override
public Boolean call() throws Exception {
//获取文件名
String httpFileName = HttpUtils.getFileName(url);
//分块文件名
httpFileName = httpFileName + ".temp" + part;
//下载路径
httpFileName = Constant.PATH;
//获取分块下载链接
HttpURLConnection httpURLConnection = HttpUtils.getHttpURLConnection(url, startPos, endPos);
try (
InputStream inputStream = httpURLConnection.getInputStream();
BufferedInputStream bis = new BufferedInputStream(inputStream);
RandomAccessFile accessFile = new RandomAccessFile(httpFileName, "rw");
) {
byte[] buffer = new byte[Constant.BYTE_SIZE];
int len = -1;
while ((len = bis.read()) != -1) {
accessFile.write(buffer, 0, len);
}
} catch (FileNotFoundException e) {
LogUtils.error("下载的文件不存在{}",url);
return false;
} catch (Exception e) {
LogUtils.error("文件下载异常");
return false;
}finally {
httpURLConnection.disconnect();
}
return true;
}
}
DownloadTask类实现了Callable接口,因此它可以被多个线程并发调用。在多线程环境下,当需要下载文件的一部分时,可以创建一个DownloadTask对象,并将其提交给线程池执行。线程池会自动分配一个线程来执行该任务,并返回一个Future对象,用于获取任务执行的结果。
/**
* 文件切片
*
* @param url 文件地址
* @param futureList 切片文件的列表
*/
public void split(String url, List<Future> futureList) {
//TODO 动态计算文件块的数量,过后以64mb为一块
try {
//获取文件下载大小
long contentLength = HttpUtils.getHttpFileContentLength(url);
//计算切分后的文件大小
long size = contentLength / Constant.THREAD_NUM;
//计算分块个数
for (int i = 0; i < Constant.THREAD_NUM; i++) {
//计算下载起始位置
long startPos = i * size;
//计算结束位置
long endPos;
if (i == Constant.THREAD_NUM - 1) {
//下载最后一块
endPos = 0;
} else {
endPos = startPos + size;
}
//如果不是第一块数据,起始位置要+1
if (startPos != 0) {
startPos++;
}
//创建下载任务对象
DownloadTask downloadTask = new DownloadTask(url, startPos, endPos, i);
//提交到线程池
Future<Boolean> future = poolExecutor.submit(downloadTask);
futureList.add(future);
}
} catch (IOException e) {
e.printStackTrace();
}
}
代码解释:
原子类(Atomic)是一种特殊的数据类型,它可以保证在多线程环境下对共享数据的访问是原子性的。这意味着在一个线程修改一个原子变量时,其他线程无法同时访问该变量,从而避免了数据不一致的问题。
修改成原子类的原因主要有以下几点:
修改上文文件下载信息类com.core.DownloadInfoThread
将finishedSize,downSize两个字段从double类型修改为LongAdder类型并初始化,run()方法中两个字段的值使用doubleValue()进行调用
package com.core;
import com.constant.Constant;
import java.util.concurrent.atomic.LongAdder;
/**
* @Author YZK
* @Date 2023/10/19
* 展示下载信息
*/
public class DownloadInfoThread implements Runnable {
//下载文件总大小
private long httpFileContentLength;
//本地已经下载文件的大小
public static LongAdder finishedSize=new LongAdder();
//本次累计下载的大小
public static volatile LongAdder downSize=new LongAdder();
//前一次下载的大小
public double prevSize;
public DownloadInfoThread(long httpFileContentLength) {
this.httpFileContentLength = httpFileContentLength;
}
@Override
public void run() {
//计算文件总大小,mb
String httpFileSize = String.format("%.2f", httpFileContentLength / Constant.MB);
//计算每秒下载速度,kb
int speed = (int) ((downSize.doubleValue() - prevSize) / 1024d);
prevSize = downSize.doubleValue();
//计算剩余文件大小
double remainSize = httpFileContentLength - finishedSize.doubleValue() - downSize.floatValue();
//计算剩余时间
String remainTime = String.format("%.1f", remainSize / 1024d / speed);
if ("Infinity".equalsIgnoreCase(remainTime)) {
remainTime = "-";
}
//已下载文件大小
String currentFileSize = String.format("%.2f", (downSize.doubleValue() - finishedSize.doubleValue()) / Constant.MB);
String downInfo = String.format("已下载%smb/%smb,速度%skb/s,剩余时间%ss", currentFileSize, httpFileSize, speed, remainTime);
System.out.print("\r");
System.out.print(downInfo);
}
}
修改com.core.Downloader,在里面添加两个方法merge,clearTemp,并增加CountDownLatch来阻塞线程
package com.core;
import com.constant.Constant;
import com.util.FileUtils;
import com.util.HttpUtils;
import com.util.LogUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* @Author YZK
* @Date 2023/10/18
* 下载器核心类
*/
public class Downloader {
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
Constant.THREAD_NUM,
Constant.THREAD_NUM,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(Constant.THREAD_NUM)
);
private CountDownLatch countDownLatch = new CountDownLatch(Constant.THREAD_NUM);
public void download(String url) {
//获取文件名
String httpFileName = HttpUtils.getFileName(url);
//拼接文件下载路径
httpFileName = Constant.PATH + httpFileName;
//获取本地文件大小
long localFileLength = FileUtils.getFileContentLength(httpFileName);
//获取链接对象
HttpURLConnection httpURLConnection = null;
DownloadInfoThread downloadInfoThread = null;
try {
httpURLConnection = HttpUtils.getHttpURLConnection(url);
//获取下载文件的总大小
int contentLength = httpURLConnection.getContentLength();
//判断文件是否已经下载过
//Todo 过后使用MD5进行校验
if (localFileLength >= contentLength) {
LogUtils.info("{}文件已经下载完毕,无需重新下载", httpFileName);
return;
}
downloadInfoThread = new DownloadInfoThread(contentLength);
//将任务交给线程执行,延迟一秒执行,然后每隔一秒执行一次
scheduledExecutorService.scheduleAtFixedRate(downloadInfoThread, 1, 1, TimeUnit.SECONDS);
//切分任务
ArrayList<Future> list = new ArrayList<>();
split(url, list);
countDownLatch.await();
//合并文件
if (merge(httpFileName)) {
clearTemp(httpFileName);
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println("\r");
System.out.print("下载完成");
//关闭连接对象
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
//关闭
scheduledExecutorService.shutdownNow();
//关闭线程池
poolExecutor.shutdown();
}
}
/**
* 文件切片
*
* @param url 文件地址
* @param futureList 切片文件的列表
*/
public void split(String url, List<Future> futureList) {
//TODO 动态计算文件块的数量,过后以64mb为一块
try {
//获取文件下载大小
long contentLength = HttpUtils.getHttpFileContentLength(url);
//计算切分后的文件大小
long size = contentLength / Constant.THREAD_NUM;
//计算分块个数
for (int i = 0; i < Constant.THREAD_NUM; i++) {
//计算下载起始位置
long startPos = i * size;
//计算结束位置
long endPos;
if (i == Constant.THREAD_NUM - 1) {
//下载最后一块
endPos = 0;
} else {
endPos = startPos + size;
}
//如果不是第一块数据,起始位置要+1
if (startPos != 0) {
startPos++;
}
//创建下载任务对象
DownloadTask downloadTask = new DownloadTask(url, startPos, endPos, i,countDownLatch);
//提交到线程池
Future<Boolean> future = poolExecutor.submit(downloadTask);
futureList.add(future);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/***
* 文件合并
* @param fileName 文件名
* @return boolean
*/
public boolean merge(String fileName) {
LogUtils.info("开始合并文件{}", fileName);
byte[] buffer = new byte[Constant.BYTE_SIZE];
int len = -1;
try (RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw")) {
for (int i = 0; i < Constant.THREAD_NUM; i++) {
try (BufferedInputStream bis = new BufferedInputStream(
Files.newInputStream(Paths.get(fileName + ".temp" + i)))) {
while ((len = bis.read(buffer)) != -1) {
accessFile.write(buffer, 0, len);
}
}
}
LogUtils.info("文件合并完毕");
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/***
* 清楚临时文件
* @param fileName
* @return
*/
public boolean clearTemp(String fileName) {
for (int i = 0; i < Constant.THREAD_NUM; i++) {
File file = new File(fileName + ".temp" + i);
file.delete();
}
return true;
}
}
修改com.core.DownloadTask
增加一个CountDownLatch字段,并修改构造方法,最后在finaly中添加 countDownLatch.countDown();
package com.core;
import com.constant.Constant;
import com.util.HttpUtils;
import com.util.LogUtils;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
/**
* @Author YZK
* @Date 2023/10/25
*/
public class DownloadTask implements Callable<Boolean> {
private String url;
//下载的起始位置
private long startPos;
//下载的结束位置
private long endPos;
//标识当前文件是第几部分
private int part;
private CountDownLatch countDownLatch;
public DownloadTask(String url, long startPos, long endPos, int part, CountDownLatch countDownLatch) {
this.url = url;
this.startPos = startPos;
this.endPos = endPos;
this.part = part;
this.countDownLatch = countDownLatch;
}
@Override
public Boolean call() throws Exception {
//获取文件名
String httpFileName = HttpUtils.getFileName(url);
//分块文件名
httpFileName = httpFileName + ".temp" + part;
//下载路径
httpFileName = Constant.PATH + httpFileName;
//获取分块下载链接
HttpURLConnection httpURLConnection = HttpUtils.getHttpURLConnection(url, startPos, endPos);
try (
InputStream inputStream = httpURLConnection.getInputStream();
BufferedInputStream bis = new BufferedInputStream(inputStream);
RandomAccessFile accessFile = new RandomAccessFile(httpFileName, "rw");
) {
byte[] buffer = new byte[Constant.BYTE_SIZE];
int len = -1;
while ((len = bis.read(buffer)) != -1) {
//1秒内下载数据之和通过原子类进行操作
DownloadInfoThread.downSize.add(len);
accessFile.write(buffer, 0, len);
}
} catch (FileNotFoundException e) {
LogUtils.error("下载的文件不存在{}", url);
return false;
} catch (Exception e) {
LogUtils.error("文件下载异常");
return false;
} finally {
httpURLConnection.disconnect();
countDownLatch.countDown();
}
return true;
}
}
笑的像个两百斤的孩子-Gitee仓库-Java多线程下载器
**问题:**写这个方法时,else的判断语句中,最后在setRequestProperty方法中,传第二个参数时,少拼接了一个"-",就导致文件切片不能下载完全,只能下载前面的几个部分
**原因:**因为没有拼接最后的"-",导致文件的size超出了RANGE。
**问题:**输入链接地址开始下载后,下载区间的信息没有问题,文件也能正常的下载,一直执行不到finaly中,导致文件下载完成后,下载信息一直重复累加,出现下图情况。
原因:
如果在调用 read() 方法时不传入缓冲区,那么该方法将直接从输入流中读取数据,并将其转换为字节数组。这意味着每次调用 read() 方法都会返回实际读取到的字节数,而不会使用缓冲区来暂存数据。
【动力节点】Java多线程下载器项目实战