7.2 多线程下载器



之前学习了集合、泛型、多线程、I/O四个Java最常用的技术,那这些技术在实际开发中如何使用呢?

以综合案例:多线程下载器,将上述技术串联起来。

也是一个非常实用的小工具。

一、批量下载器

1.1需求分析

借鉴迅雷多线程下载的思路,多个任务并行运行。因此,具体的功能有:

(1)通过读取源文件,多线程自动下载所有网络资源到本地

(2)遇到下载故障,在控制台上打印错误信息

(3)允许自定义源文件的地址,保存下载文件的目录要自动创建

(4)允许自定义同时下载的任务数量,不指定默认开启10个下载任务

(5)下载文件的文件名,要与网址包含文件名保持一致

(6)下载成功后,在控制台输出存储路径与文件尺寸

1.2 实现思路

1.3 成品演示

略。

二、多线程资源下载及异常处理

2.1 封装URLConnection单文件下载器

1.多线程下载器,其实是基于单文件下载器的,然后并行运行。所以,先实现单文件的下载和保存操作:

public class Downloader {
    /**
     * 下载多个文件保存到本地
     * @param source 原图片的网址
     * @param targetDir 目标目录,要确保已存在
     */
    public void download(String source, String targetDir) {
        InputStream is = null;  //用于读取来自网络的数据流
        OutputStream os = null; //用于向指定文件写入

        try {
            //示例:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11572548.jpeg
            //注意:是jpeg,不是jpg!
            String fileName = source.substring(source.lastIndexOf("/" )+1);     //从p开始读取,获取文件名    //1.注意+1的位置
            File targetFile = new File(targetDir + "/" + fileName);     //定义目标文件

            if (!targetFile.exists()) {     //当前的目标文件,可能不存在,所以要判断一下
                targetFile.createNewFile();
            }

            URL url = new URL(source);  //实例化一个URL对象,将原图片网址的参数传进去
            URLConnection connection = url.openConnection();
            is = connection.getInputStream();     //得到网络上图片的输入流

            os = new FileOutputStream(targetFile);  //向上面已创建好的目标文件,进行写入

            byte[] bs = new byte[1024];        //通过字节流来读取is对象       //最多读1k字节
            int len = 0;
            while ((len = is.read(bs)) != -1) {     //当网络上图片的输入流is还没读完时
                //一边读一边写
                os.write(bs, 0, len);   //要读取的内容+从哪里开始读+有效的部分
            }
            //友好:               一般性消息;              换行,缩进,                          防止出现小数,向下取整
            System.out.println("[INFO]图片下载完毕:"+source+"\n\t ->"+targetFile.getPath()+"("+Math.floor(targetFile.length()/1024)+"kb)");

        } catch (IOException e) {
            e.printStackTrace();
        } finally {     //关闭流,也要try-catch进行包裹,因为关闭流也可能会抛出I/O异常
            try {
                if (os != null) {
                    os.close();     //关闭os流
                }
                if (is != null) {
                    is.close();     //关闭is流
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Downloader downloader = new Downloader();
        downloader.download("https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11572548.jpeg","e:/a/d");
    }
}

--->
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11572548.jpeg
     ->e:\a\d\pexels-photo-11572548.jpeg(49.0kb)

成功保存到了本地目录中:

2.拓展:创建对象时,构造参数的提示,在哪里设置?每次鼠标到那里就不提示。

(或者可以提升为:方法使用时,如何自动显示参数的提示)

  • 问题:

  • 解决方案:

​ 勾选上以下两项;

​ 如果参数列表提示几秒钟消失了,可以使用快捷键Ctrl+P显示参数。

2.2 读取config.properties配置文件

1.Properties类

(1)Properties类是java.utils包专用于读取应用属性文件的类

(2)Properties文件存储数据的形式是:属性名=属性值

(3)多线程下载器,用config.properties来保存程序的配置信息

其中,config是文件名,可以改;properties是扩展名格式,不能改。

2.读取config.properties配置文件

以上是写死了目标文件的目录,如果要调整,就要修改源代码,然后重新编译,甚至重新部署和发布,着非常麻烦。

因此,应该将未来可能会灵活调整的配置项,独立出来放到一个文件中。

src目录下,新建文件:config.properties,放一些未来可能会灵活调整的配置项:

在文件config.properties中:

thread-num=10;      //配置文件的书写都是紧密的,不能出现空格
target-dir=e:/a/d

以上是错误的写法,正确的是:

  • 不能有空格、空行
  • 也不能有分号
thread-num=10
target-dir=e:/a/d
config.properties配置文件在格式上,很娇气~

3.Properties类的使用

public class Downloader {
    public void download(String source, String targetDir) {
        ...

    public static void main(String[] args) {
        File propFile = new File("E:\\code\\downloader\\src\\config.properties");   //创建一个文件的对象,放入配置文件的路径

        Properties properties = new Properties();        //实例化Properties类
        Reader reader=null;
        try {
            reader = new FileReader(propFile);
            properties.load(reader);      //加载指定的字符输入流,以加载配置文件
            String threadNum = properties.getProperty("thread-num");       //读取配置文件的内容(键值对)
            String targetDir = properties.getProperty("target-dir");

            System.out.println(threadNum);  //检查下配置文件获取是否正常
            System.out.println(targetDir);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        Downloader downloader = new Downloader();
        downloader.download("https://manongbiji.oss-cn-beijing.aliyuncs.com/images/weixin.jpg","e:/a/d");
    }
}

--->
10;      
e:/a/d
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/images/weixin.jpg
     ->e:\a\d\weixin.jpg(22.0kb)

4.结合工程得到使用

public class Downloader {  
    ...
    //开始多线程下载
    public void start(String propDir) {        //参数:配置文件的目录
        File propFile = new File(propDir+"\\config.properties");  //创建一个文件对象,放入配置文件的路径

        Properties properties = new Properties();
        Reader reader=null;
        try {
            reader = new FileReader(propFile);
            properties.load(reader);      //加载指定的字符输入流,以加载配置文件
            String threadNum = properties.getProperty("thread-num");       //读取位置文件的内容
            String targetDir = properties.getProperty("target-dir");

            System.out.println(threadNum);  //检查下配置文件获取是否正常
            System.out.println(targetDir);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public static void main(String[] args) {
        Downloader downloader = new Downloader();
        downloader.start("E:\\code\\downloader\\src"); //只需要传入src文件的目录的路径
    }
}

--->
10;          //成功读取到了config.properties配置文件中的配置项
e:/a/d

5.拓展:

  • 错误的信息

  • 正确的做法:

​ 应该是独立的实例化,然后将对象声明放在try块外面,用于关闭。

2.3 读取download.txt下载列表

所有的准备从网络上下载资源的网址,都放在了download.txt文本文件中。

下面通过Java I/O的方式,去读取它。

其中,download.txt文本文件必须与上节的配置文件,放在同一级目录下。

  • download.txt中:
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11572548.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11593467.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11631922.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12203460.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12240136.jpeg
  • Downloader.java中:
public class Downloader {
    /**
     * 从指定文件读取下载地址,批量下载网络资源
     * @param targetDir 下载文件的存储目录
     * @param downloadTxt download.txt完整路径
     */
    public void multiDownloadFromFile(String targetDir, String downloadTxt) {
        File dir = new File(targetDir);    //创建一个文件对象,放入下载文件的存储目录
        if (!dir.exists()) {        //如果目录不存在,就创建一个
            dir.mkdirs();
        }

        System.out.println("[INFO]发现下载目录[" + dir.getPath() + "]不存在,已自动创建"); //给个友好的提示
        //一行一行的读取download.txt中内容、
        List<String> resources = new ArrayList();   //定义一个数组集合,用于存放读取来的数据
        BufferedReader reader = null;      //用缓冲区的Reader对象,进行暂时缓存
        try {
            reader = new BufferedReader(new FileReader(downloadTxt));   //FileReader的简化使用:套娃形式
            String line = null;         //代表每一行的文本
            while ((line = reader.readLine()) != null) {    //判空,如果还有新的一行文本
                resources.add(line);
                System.out.println(line);   //验证下,line是否读取成功
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (reader != null) {
                try {
                    reader.close();     //上述套娃的使用形式,只关闭最外侧流即可,内侧流会自动关闭
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    //开始多线程下载
    public void start(String propDir) {
        File propFile = new File(propDir+"\\config.properties");  

        Properties properties = new Properties();
        Reader reader=null;
        try {
            reader = new FileReader(propFile);
            properties.load(reader);     
            String threadNum = properties.getProperty("thread-num");      
            String targetDir = properties.getProperty("target-dir");

            this.multiDownloadFromFile(targetDir, propDir + "\\download.txt");    //调用下面刚定义好的方法

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public static void main(String[] args) {
        Downloader downloader = new Downloader();
        downloader.start("E:\\code\\downloader\\src");     //只需要传入src文件的目录的路径
    }
}

--->
[INFO]发现下载目录[e:\a\d]不存在,已自动创建
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11572548.jpeg        //成功读取
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11593467.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11631922.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12203460.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12240136.jpeg

2.4 实现多线程下载及本地文件保存

线程池的种类有四种,这里使用的是定长线程池。

将每一个网址,对应一个download的下载任务,来进行并发下载。

public class Downloader {
    private Integer threadNum = 10;

    /**
     * 从指定文件读取下载地址,批量下载网络资源
     * @param targetDir 下载文件的存储目录
     * @param downloadTxt download.txt完整路径
     */
    public void multiDownloadFromFile(String targetDir, String downloadTxt) {
        File dir = new File(targetDir);    
        if (!dir.exists()) {       
            dir.mkdirs();
        }

        System.out.println("[INFO]发现下载目录[" + dir.getPath() + "]不存在,已自动创建"); 
        List<String> resources = new ArrayList();  
        BufferedReader reader = null;    

        ExecutorService threadPool = null;    //声明一个线程池,放在try块外面是为了后续方便关闭

        try {
            reader = new BufferedReader(new FileReader(downloadTxt));  
            String line = null; 
            while ((line = reader.readLine()) != null) {   
                resources.add(line);
                System.out.println(line); 
            }
            threadPool = Executors.newFixedThreadPool(this.threadNum);   //创建线程池,定长10个
            //用for循环遍历出每个要下载的资源
            Downloader that = this;     //声明一个外部类的对象,暂时换个名儿
            for (String res : resources) {              //res:对应每一个网址
                threadPool.execute(new Runnable() {     //简洁写法,自动创建一个内部的匿名类
                    @Override
                    public void run() {
                        //this.download         //this指的是内部的匿名类的对象。因为在不同的类里,this指向的对象是不一样的
                        that.download(res, targetDir);  //并行下载:就是调用download方法
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (threadPool != null) {
                threadPool.shutdown();  //等所有线程处理完毕,就关闭掉线程池     //销毁有10个跑道的小操场
            }
            if (reader != null) {
                try {
                    reader.close();     
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    //开始多线程下载
    public void start(String propDir) {
        File propFile = new File(propDir+"\\config.properties");  
        Properties properties = new Properties();
        Reader reader=null;
        try {
            reader = new FileReader(propFile);
            properties.load(reader);      
            String threadNum = properties.getProperty("thread-num");       
            this.threadNum = Integer.parseInt(threadNum);   //将字符串类转换成整型包装类
            String targetDir = properties.getProperty("target-dir");
            this.multiDownloadFromFile(targetDir, propDir + "\\download.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Downloader downloader = new Downloader();
        downloader.start("E:\\code\\downloader\\src"); 
    }
}

--->
[INFO]发现下载目录[e:\a\d]不存在,已自动创建
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11572548.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11593467.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11631922.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12203460.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12240136.jpeg
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11593467.jpeg
     ->e:\a\d\pexels-photo-11593467.jpeg(36.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12240136.jpeg
     ->e:\a\d\pexels-photo-12240136.jpeg(83.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12203460.jpeg
     ->e:\a\d\pexels-photo-12203460.jpeg(90.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11572548.jpeg
     ->e:\a\d\pexels-photo-11572548.jpeg(49.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11631922.jpeg
     ->e:\a\d\pexels-photo-11631922.jpeg(214.0kb)

成功下载了上述资源:

2.5 更充分的演示程序

  • download.txt中,增加更多的下载资源:
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11572548.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11593467.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11631922.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12203460.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12240136.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-10092819.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-10519162.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-10977780.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-11311195.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-11954484.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-12072057.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-12072057.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-4542338.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-6323680.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-6873171.jpeg
https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-7409835.jpeg
  • 在文件config.properties中,调整配置项:
thread-num=5
target-dir=e:/c/f
  • 代码执行结果:
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11593467.jpeg
     ->e:\c\f\pexels-photo-11593467.jpeg(36.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11572548.jpeg
     ->e:\c\f\pexels-photo-11572548.jpeg(49.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12240136.jpeg
     ->e:\c\f\pexels-photo-12240136.jpeg(83.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-12203460.jpeg
     ->e:\c\f\pexels-photo-12203460.jpeg(90.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-10977780.jpeg
     ->e:\c\f\pexels-photo-10977780.jpeg(62.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/pexels-photo-11631922.jpeg
     ->e:\c\f\pexels-photo-11631922.jpeg(214.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-11954484.jpeg
     ->e:\c\f\pexels-photo-11954484.jpeg(15.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-12072057.jpeg
     ->e:\c\f\pexels-photo-12072057.jpeg(41.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-10519162.jpeg
     ->e:\c\f\pexels-photo-10519162.jpeg(13.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-4542338.jpeg
     ->e:\c\f\pexels-photo-4542338.jpeg(36.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-6323680.jpeg
     ->e:\c\f\pexels-photo-6323680.jpeg(59.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-6873171.jpeg
     ->e:\c\f\pexels-photo-6873171.jpeg(44.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-10092819.jpeg
     ->e:\c\f\pexels-photo-10092819.jpeg(32.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-12072057.jpeg
     ->e:\c\f\pexels-photo-12072057.jpeg(41.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-7409835.jpeg
     ->e:\c\f\pexels-photo-7409835.jpeg(76.0kb)
[INFO]图片下载完毕:https://manongbiji.oss-cn-beijing.aliyuncs.com/imooc/pexels/s/pexels-photo-11311195.jpeg
     ->e:\c\f\pexels-photo-11311195.jpeg(75.0kb)
  • 成功下载了上述资源:

声明:Jerry's Blog|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 7.2 多线程下载器


Follow excellence, and success will chase you.