MP3在线搜索下载程序

来源:互联网 发布:网络说唱歌手排行榜 编辑:程序博客网 时间:2024/04/28 00:28

 

1.1 案例介绍

本案例是一个MP3在线搜索程序,输入歌曲的名字,就可以在互联网上搜索和下载歌曲。支持多线程并发下载。

1.1.1 目的和意义

MP3下载是一个非常有价值的应用。这个应用有两个典型特点:1)访问互联网,需要强大的网络功能支持;2)需要多线程并发运行,能够同时下载多首歌曲。

Java本身提供了强大的网络功能,能够非常容易的辨析网络通信程序,即利用JDK的API就能够直接编写下载互联网上相关资源的程序。同时,Java本身支持多线程机制,并且JDK5,6又增强了对多线程并发程序的支持,提供了很多支持高级并发特性的API。

用Java编写一个MP3在线搜索下载程序,不仅能满足用户下载MP3的需要,而且能够展示Java的网络和并发方面的强大功能,符合本课程的主题。

1.1.2 主要界面

1. 程序启动后显示搜索界面

2. 选择搜索结果并添加为下载任务

3. 启动下载任务后的界面,程序设置为只能运行3个下载任务,其他等待

4. 下载完成后的界面

5. 下载过程中,继续搜索其他歌曲的界面

1.1.3 主要功能

从上一小节的界面中基本上可以看出本程序的主要功能:

1) 输入歌名搜索歌曲;

2) 选择多个搜索结果,添加为下载任务;

3) 在下载的过程中继续搜索其他歌曲;

4) 可以开始多个下载任务;

5) 可以暂停多个下载任务;

6) 可以停止多个下载任务;

7) 可以删除多个下载任务;

8) 可以输出程序执行过程;

9) 可以设置歌曲保存路径;

10) 可以显示下载任务的进度、速度等。

11) 自动删除已经完成的任务。

1.1.4 主要操作流程


1.2 安装运行

本程序的安装和运行非常简单。本程序以rar压缩文件的形式存在。文件名为MyMP3Searcher.rar

1.2.1 配置开发环境

1) 利用了Swing应用程序框架(AppFramework)开图形界面,需要appframework-1.0.3.jar,swing-worker-1.1.jar。

2) MyMP3Searcher.rar解压缩到一个文件夹下,然后作为项目导入Eclipse 3.4中就可以编译运行。

3) 使用了Amino提供的无锁数据结构,需要amino-cbbs-0.3.2.jar。

4) 需要JDK6支持,并且所有的第三方包已经在压缩文件MyMP3Searcher.rar中。

1.2.2 运行

本程序已经打包成jar可执行程序:MyMP3Searcher.jar。已经把需要的第三方支持包放在了lib文件夹下。如下图:

直接双击MyMP3Searcher.jar就可以运行。

或者在命令行输入:java -jar "MyMP3Searcher.jar"

1.3 程序分析

分析程序的执行流程和主要功能的实现,并对主要的类进行说明。

1.3.1 程序执行流程


1.3.2 多线程并发分析

在程序执行的过程中,会产生多个线程:

1) 搜索MP3的线程。搜索完成后线程终止;可以多次启动搜索不同的歌曲。

2) 使用了多个线程更新表格和进度条。因为每个任务的下载进度不断发生变化,并且任务数也是不断变化的,并且Swing大部分API是非线程安全的。

3) 多个下载线程。本程序限制为同时只能运行3个线程。设置了一个信号量Semaphore进行控制,下载任务开始后,需要获得许可才能运行,结束后释放许可。

4) 检查下载任务是否完成并删除的线程。

设置一个定时启动线程TimeTask,周期性的检查TaskTableModel中的任务的执行情况。并进行表格更新。这样TaskTableModel就是多个线程共享的资源,对其操作包括增加任务、删除任务等。同时,使用了一个Amino提供的无锁数据结构LockFreeList,当检查有已经完成的任务时,利用线程添加到LockFreeList中。本想用LockFreeList替换TaskTableModel中的ArrayList,但是LockFreeList中很多功能没有实现。等功能完成后,再替换。

5) 使用了原子量AtomicInteger为下载的歌曲文件产生了唯一的序号。

6) 线程的停止使用了状态量和中断方法。

7) 添加下载任务时,使用Downloader中的异步线程,获取歌曲的大小。

8) 在处理下载任务时,多个线程需要访问TaskTableModel等共享资源,使用了同步技术和Amino无锁数据结构。

1.3.3 类说明

本程序主要包含的类分为三个包:mymp3包里面是程序的主要类;mymp3.downloader是专门负责下载功能的类;3)mymp3.support是一些辅助功能的类。

类名字

说明

mymp3

AppConfig

定义了程序的一些配置信息,如文件的保存路径。定义了信号量、序数产生器,定义了常量控制运行同时运行的线程数。

AtomicCounter

唯一序数生成器,使用了JDK提供的原子类。

MP3URLParser

这是一个比较关键的类。因为百度mp3改变了歌曲URL地址的表示方式,不能直接在Html源代码中获取。百度Mp3对歌曲的url地址进行了加密,并在html中使用javascript函数进行解密。本类就是把百度自动生产的javascript函数解析成Java类,进行解密获取歌曲url地址。

MYmp3AboutBox

显示一个关于对话框。

MYmp3App

由Swing应用程序框架定义的应用程序主类。程序从这里启动。

MYmp3View

程序的主界面类。包含了各种按钮的事件处理代码。包含了两个内部类:MGroup和SearchMP3。MGroup表示检索结果中的一行歌曲(解析前)。SearchMP3是具体辅助搜索MP3的线程。

MP3Model

表示解析后的检索结果存储表格的一行数据。包含歌曲的名字,URL和大小。

Mp3TableModel

存储检索结果的表格的模型。

StringFilter

解析Html用的工具类,包含了一些儿字符的处理方法。

TextOutput

用于输出执行结果的工具类。

mymp3.downloader

Downloader

具体执行歌曲下载的线程。

Manager

管理下载任务的类。内部类CheckOKTask定期检查是否有已经完成的任务。

TaskModel

表示一个下载任务的类。包含的数据有:状态、文件名、大小、速度、已下载、剩余时间等。

TaskTableModel

存储下载任务的表格的模型。

mymp3.support

DirFileFilter

文件过滤辅助类,设置存储路径的时候使用。

MyTableCellRenderer

渲染表格单元的工具类。

ProgressBarRenderer

渲染进度条的工具类。

更详细的说明可以参考源代码及其中的注释。

1.3.4 主要功能实现分析

(1)Mp3搜索功能分析

在主界面启动点击“搜索MP3”按钮后,启动搜索线程。如下面的代码:

private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {                                         

        this.searchMp3();

}   

this.searchMP3()方法启动一个线程,如下:

private void searchMp3() {

        search = new Thread(new SearchMp3());

        search.start();

}

下面看一下SearchMp3类。因为Swing很多类不是线程安全的,定义了一些匿名的线程目标对象,使用SwingUtilities.invokeLater()进行调用,进行界面更新。

private class SearchMp3 implements Runnable {

        private int read;

        private int total;

        Runnable updateBefore = new Runnable() {

            public void run() {

                progressBar.setVisible(true);

                progressBar.setIndeterminate(true);

                progressBar.setStringPainted(false);

            }

        };

        Runnable beforeProcess = new Runnable() {

            public void run() {

                progressBar.setIndeterminate(false);

                progressBar.setStringPainted(true);

            }

        };

        //更新表格显示的线程

        Runnable update1 = new Runnable() {

            public void run() {

                jTable1.updateUI();

                if (total <= 0) {

                    return;

                }

                progressBar.setValue(Integer.parseInt(

                        String.valueOf(read * 100 / total)));

            }

        };

        Runnable searchOver = new Runnable() {

            public void run() {

                progressBar.setVisible(false);

            }

        };

        public void run() {

            try {

                //启动更新进度条的线程

                SwingUtilities.invokeLater(updateBefore);

                String keyword = URLEncoder.encode(jtf1.getText());

                //向百度提交搜索请求的URL

                String uStr = "http://mp3.baidu.com/m?f=ms&tn=baidump3&ct=134217728&lf=&rn=&word=" + keyword + "&lm=-1";

                //连接服务器获取搜索结果

                String listPageCode = StringFilter.getHtmlCode(uStr);

                //对搜索结果进行解析

                //去除搜索结果的头部

                String[] temp = listPageCode.split("链接速度[/r/n/t]*</th>[/r/n/t]*</tr>[/r/n/t]*<tr>");

                if (temp.length >= 2) { // temp小于2则表示找不到数据

                    //去除搜索结果的尾部

                    temp = temp[1].split("</tr>[/r/n/t]*</table>");

                    //把中间的搜索结果按行分割

                    temp = temp[0].split("</tr><tr>");//

                    if (temp.length > 0) {

                        total = temp.length;

                        Mp3TableModel mtm = (Mp3TableModel) jTable1.getModel();

                        mtm.clearValues();

                        SwingUtilities.invokeLater(beforeProcess);

                        for (String group : temp) {//解析每一行

                            read++;

                            MGroup mg = new MGroup(group);  // 第一个页面数据

                            String url = mg.getURL();

                            url = url.replaceAll(mg.getName(), URLEncoder.encode(mg.getName()));

                            //访问网络获取这一行的歌曲数据

String mp3PageCode = StringFilter.getHtmlCode(url);

                            //获取每一首歌的实在的url

                            String mp3Url = getMp3Address(mp3PageCode);

                            Mp3Model mp3 = new Mp3Model(mg.getName(), mp3Url, mg.getSize());

                            mtm.addValue(mp3);

                            //调用线程更新表格

                            SwingUtilities.invokeLater(update1);

                            if (read >= 20) {

                                break;

                            }

                        }

                    }

                }

                //完成后更新进度条

                SwingUtilities.invokeLater(searchOver);

            } catch (Exception e) {

                //System.out.println("Exception e");

            }

        }

    }

上面代码的run方法,是线程的主体。使用了工具类StringFilter获取检索结果并进行解析。

解析的思路是:

1) 因为百度的mp3搜索结果前后包含很多无用的广告信息等。用tringFilter.getHtmlCode()方法获得结果,需要进行分析,去掉无用信息。该工具类使用正则表达式进行分割,把那些多余的信息去掉,留下中间的歌曲信息,然后再分割成行,存储在MGroup中。分割后的结果如下:

2) 这时还不能获取歌曲的下载地址。MGroup对象中存储的是歌曲所在页面的url地址。再次调用tringFilter.getHtmlCode()获取歌曲所在的页面。

3) 对歌曲页面的解析使用了方法getMp3Address()。因为百度mp3对歌曲进URL进行了加密,所以需要进行调用一个工具类MP3URLParser进行解密。

    private String getMp3Address(String htmlCode) {

        MP3URLParser parser = new MP3URLParser();

        parser.parseVars(htmlCode);

        htmlCode = parser.parse();

        System.out.println(htmlCode);

        return htmlCode;

    }

4) 每一行搜索结果解析成功后,加入到表格的模型Mp3TableModel中,然后更新表格的显示。

(2)添加下载任务

在搜索结果中,选择需要下载的行,然后单击“添加任务”。就把需要下载的歌曲添加的下载任务表格。主要有下面的代码:

    private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {                                         

        textOutput.append("添加下载任务/n");

        addTasks();

        jTable2.updateUI();

        

    }     

主要的是addTasks()方法:

    private void addTasks() {

        ArrayList<TaskModel> mp3ToLoad = new ArrayList<TaskModel>();

        TableModel tableModel = jTable1.getModel();

        int[] keys = jTable1.getSelectedRows();

        if (tableModel instanceof Mp3TableModel) {

            Mp3TableModel mtm = (Mp3TableModel) tableModel;

            List<Mp3Model> mp3s = mtm.getValues();

            for (int key : keys) {

                Mp3Model mp3 = mp3s.get(key);

                TaskModel tm = new TaskModel(mp3.getName().trim()+ AppConfig.getInstance().getCounter().increment(), mp3.getUrl());

                mp3ToLoad.add(tm);

                textOutput.append(tm+"/n");

            }

        }

        if (manager == null) {

            manager = new Manager(jTable2);

            TableColumnModel tcm = jTable2.getColumnModel();

            TableColumn tc = tcm.getColumn(TaskModel.COLUMN_PROCESS);

            tc.setCellRenderer(new ProgressBarRenderer());

        }

        if (!mp3ToLoad.isEmpty()) {

            manager.addTasks(mp3ToLoad);

        }

    }

其主要内容是:创建下载任务管理对象Manager;为每一个下载任务创建TaskModel对象。

每个TaskModel对象关联了一个下载对象Downloader,负责下载。

    public TaskModel(String name, String url) {

        this.name = name;

        this.url = url;

        // 在下载任务中,直接创建了下载器

        this.downloader = new Downloader(this); // 添加下载任务

    }

在创建Downloader的对象时,其构造方法,启动了一个线程获取歌曲的实际大小。

    public Downloader(TaskModel taskModel) {

        textOutput = new TextOutput();

        //下载程序执行的任务

        this.task = taskModel;

        //创建一个异步线程进行文件大小初始化

        Thread tt = new Thread(new Init());

        tt.start();

    }

    private class Init implements Runnable {

        public void run() {

            init();

        }

    }

    /**获取文件大小

     */

    private void init() {

        try {

            if (totalBytes <= 0) {

                URL u = new URL(task.getUrl());

                totalBytes = u.openConnection().getContentLength();

            }

        } catch (Exception ex) {

            System.out.println("Exception:" + this.getClass().getName());

        }

    }

为了保持歌曲名字的唯一性,在原始歌名后加一个唯一的序数,该序数使用原子类生产,封装在类AtomicCounter中。调用代码如下:

AppConfig.getInstance().getCounter().increment()

(3)启动下载任务

在下载任务窗口中,选择需要启动的任务,单击“开始”按钮。为了演示多线程并发和使用JDK提供的高级并发对象,如同步器,程序限制为同时只能运行3个线程。使用信号量Semaphore进行控制,每个线程启动后,调用信号量的acquire方法,获得许可,才能运行,未获许可,则阻塞。线程结束后释放许可。

信号量设置代码:

    private int MAX_ACTIVE_TASK = 3;

    private Semaphore semaphore;

    private AtomicCounter counter;    

private AppConfig() {

        semaphore = new Semaphore(MAX_ACTIVE_TASK);

        counter = new AtomicCounter();

    }

“开始”按钮代码:

    private void jButton3ActionPerformed(java.awt.event.ActionEvent evt) {                                         

        // 开始按钮

        TableModel tm = jTable2.getModel();

        int[] keys = jTable2.getSelectedRows();

        if (keys.length > 0 && (tm instanceof TaskTableModel)) {

            TaskTableModel ttm = (TaskTableModel) tm;

            for (int key : keys) {

                try {

                    TaskModel task = ttm.getValue(key);

                    task.toStart();

                } catch (Exception exception) {

                }

            }

        }

    }             

每个下载任务调用他的变量downloadder的toStart()启动下载线程。

    public void toStart() {

        downloader.toStart();

    }

下面是downloader的toStart()方法

    public synchronized void toStart() {

        if (isStopped()) {

            this.start();

            System.out.println("开始下载");

        } else if (isPaused()) {

            System.out.println("继续下载");

            this.notifyAll();

        }

        this.state = STATE_LOADING;

    }

线程启动后获得许可:

public void run() {

        textOutput.appendln("准备开始下载:" + this.task);

        try {

            AppConfig.getInstance().getSemaphore().acquire();

            textOutput.appendln("开始下载:" + this.task);

        } catch (InterruptedException ex) {

           ……

        }

…….

线程完成后,方法许可:

  AppConfig.getInstance().getSemaphore().release();

(4)暂停下载任务

在主界面选择需要暂停的任务,单击“暂停”就可以暂停已经运行的任务。暂停的基本原来就是阻塞线程,使其阻塞、进入等待队列。

    private void jButton4ActionPerformed(java.awt.event.ActionEvent evt) {                                         

        // 暂停按钮

        TableModel tm = jTable2.getModel();

        int[] keys = jTable2.getSelectedRows();

        if (tm instanceof TaskTableModel) {

            TaskTableModel ttm = (TaskTableModel) tm;

            for (int key : keys) {

                try {

                    TaskModel task = ttm.getValue(key);

                    task.toPause();

                } catch (Exception exception) {

                }

            }

        }

    }        

转到调用downloader的toPause方法,把状态变量设置为

    public synchronized void toPause() {

        this.state = STATE_PAUSED;

    }

在线程体run方法中循环检查状态变量的值,如果是STATE_PAUSED则,调用wait()方法阻塞。启动的时候再调用notifyAll()。

                synchronized (this) {

                    if (isPaused()) {

                        try {

                            this.wait();

                        } catch (InterruptedException ie) {

                            //  AppConfig.getInstance().getSemaphore().release();

                            System.out.println("InterruptedException");

                        }

                    }

                }

(5)停止下载任务

单击“停止”按钮后,会调用下载任务TaskModel的toStop方法。然后调用Downloader的停止方法。

    public void toStop() {

        downloader.stopDownload();

    }

通过设置中断取消线程执行。

    /** 停下下载任务 */

    public void stopDownload() {

        this.state = STATE_STOPPED;

        this.interrupt();

        System.out.println("终止");

    }

在线程体run方法中检查中断,发生中断后,停止线程,并释放许可。

                //每循环一次,检测是否被中断,如果中断,则停止

                if (Thread.interrupted()) {

                    toStop();

                    in.close();

                    out.close();

                    AppConfig.getInstance().getSemaphore().release();

                    return;

                }

            }

(6)删除下载任务

单击“删除”按钮,执行下面的代码

    private void jButton5ActionPerformed(java.awt.event.ActionEvent evt) {                                         

        removeTask();

        jTable2.updateUI();

    }   

调用TaskTableModel的方法删除任务,如果任务未完成,先停止。

    public void removeTasks(List<TaskModel> tasks) {

        //删除的时候注意同步

        for (TaskModel tm : tasks) {

            if (!tm.isOk()) {   // 如果任务未完成,则停止

                tm.toStop();

            }

            //专门演示无锁数据结构的

          //  freeLockValues.remove(tm);

        }

        synchronized (this) {

            values.removeAll(tasks);

        }

}

(7)自动检查完成任务

已经完成的任务需要从TaskTableModel中删除,并且需要随时更新正在下载的任务的进度。故在Manager类中定义了一个周期性的TimeTask任务CheckOKTask

    private class CheckOKTask extends TimerTask {

        @Override

        public void run() {

            // 检查并移除已经完成的任务

            Lock lock = new ReentrantLock(false);

            try {

                TableModel tm = jTable.getModel();

                if (tm instanceof TaskTableModel) {

                    TaskTableModel ttm = (TaskTableModel) tm;

                    List<TaskModel> tasks = ttm.getValues();

                    if (null != tasks && !tasks.isEmpty()) {

                        lock.lock();

                        synchronized (tasks) {

                            Iterator it = tasks.iterator();

                            while (it.hasNext()) {

                                TaskModel temp = (TaskModel) it.next();

                                if (temp.isOk()) {

                                    //删除的时候注意同步

                                    ttm.getFreeLockValues().add(temp);

                                    it.remove();

                                    textOutput.appendln("删除完成任务:" + temp);

                                }

                            }

                        }

                        if (tasks.isEmpty()) {

                            textOutput.appendln("所有已经完成的任务是:");

                            for (TaskModel tm2 : ttm.getFreeLockValues()) {

                                textOutput.appendln(tm2.toString());

                            }

                        }

                    }

                }

                jTable.updateUI();

            } catch (Exception exception) {

            }

        }

    }

}

(8)无锁数据结构的使用

在为文件名生成序号的时候,使用了AtomicInteger:

public class AtomicCounter {

    private static AtomicInteger value = new AtomicInteger();

    public int getValue() {

        return value.get();

    }

    public int increment() {

        return value.incrementAndGet();

    }

    public int increment(int i) {

        return value.addAndGet(i);

    }

    public int decrement() {

        return value.decrementAndGet();

    }

    public int decrement(int i) {

        return value.addAndGet(-i);

    }

}

把已经完成的任务加入到一个Amino提供的FreeLockList数据结构中。

public class TaskTableModel extends AbstractTableModel {

    private List<TaskModel> values = new ArrayList<TaskModel>();

    private List<TaskModel>  freeLockValues=new LockFreeList<TaskModel>();

……

CheckOKTask中的代码:

if (temp.isOk()) {

         //删除的时候注意同步

      ttm.getFreeLockValues().add(temp);

      it.remove();

      ……

}

1.4 总结

支持快捷的网络编程和多线程并发程序设计是Java的重要特点。本案例构建的MP3在线搜索程序充分体现了Java的这个特点。在处理多线并发的时候,使用了同步、中断、无锁数据结构等Java的多线程特性。并且使用了Amino的无锁数据结构,Amino目前虽然不是很完善,但是已经初露锋芒。

当然,本案例还有一些不完善的地方,比如不支持对同一个文件的分块下载。感兴趣的读者可以自己完善。

收集于:

http://202.202.5.145:8000/ibmjava/experiment/23.htm