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

MP3在线搜索下载程序相关推荐

  1. 案例1—MP3在线搜索下载程序

    1.1  案例介绍 本案例是一个MP3在线搜索程序,输入歌曲的名字,就可以在互联网上搜索和下载歌曲.支持多线程并发下载. 1.1.1  目的和意义 MP3下载是一个非常有价值的应用.这个应用有两个典型 ...

  2. 利用AVR单片机 专用下载 USBtinyISP对Arduino UNO下载程序

    简 介: 测试了利用USBtinyISP对于Arduino UNO,也就是ATmega系列的单片机下载Bootloader的过程.通过测试可以看到,利用USBtinyISP可以更快的下载ATmega系 ...

  3. 使用单片机对STC8G,8H,8A进行ISP下载程序

    通过第三方程序实现对STC单片机的程序下载,可以方便进行现场的调试和更新.特别是对于设计远程程序更新.无线程序下载与调试等功能有帮助. 本文给出了下载相关的一些程序设计. STC单片机ISP下载协议 ...

  4. Keil uVision5 下载程序 add flash programming algorithm选项缺少需要的下载算法的解决办法

    用Keil5在下载程序,选择下载算法时,可能会出现找不到对应芯片的情况: 这个时候就需要下载安装,Keil的Legacy support for Arm支持包了: http://www2.keil.c ...

  5. 用st-link通过stvp给stm8下载程序的坑

    用的这种st-link 包装上写着,stm32用下面得20pin,stm8用上面的4pin.然后背后有stm8的接线说明. 我接好了线,开发板没有电...测了一下红线和黑线之间电压0.7V... 只好 ...

  6. JLINK通过SW模式下载程序的方法

    JLINK通过SW模式下载程序的方法 1)概述:JLINK有2种调试模式:JTAG和SWD(串行模式).JTAG是常用模式,大家都熟悉:下载文件如图3: 2)使用SW模式,需要(只需要)4根连线,连接 ...

  7. 嵌入式linux如何下载程序,Linux平台的下载程序-嵌入式系统-与非网

    有许多网络文件下载工具可以在Windows平台下很好地工作,如NetAnts."网际快车".TelePro等,还有像WebZip那样功能强大的离线浏览器.这些工具使我们可以在Win ...

  8. IAR J-Link下载程序出现错误提示:Failed to get CPU status after 4 retries Retry?

    情况一:打开IAR的"Project"------->"Options..."------->"J-Link/J-Trace"- ...

  9. Keil MDK在个别电脑上下载程序失败的解决办法

    文章原始地址: http://feotech.com/?p=90 Keil MDK在个别电脑上无法下载程序 Keil MDK下载程序时显示 No Cortex-M SW Device Found (本 ...

最新文章

  1. 内存溢出分析之工具篇
  2. 微型计算机原理课本,微机原理与接口技术课本.doc
  3. 常用的函数式接口_Consumer接口的默认方法andThen
  4. 文献记录(part51)--识别聚类间远近关系的双几何体模型
  5. Matlab和C++混合编程
  6. zabbix server 迁移步骤
  7. java android上传文件_Java-Android-如何将txt文件上传到网站?
  8. TCP是如何保证数据的可靠传输的
  9. java mongodb 副本集,MongoDB副本集
  10. win10安装mysql报错——无法项识别为 cmdlet、函数、脚 本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
  11. Java中按值传递与按引用传递的区别
  12. java 字符串像素_如何在JavaFX中计算字符串的像素宽度?
  13. Android:如何优雅的开发马甲包?
  14. cf英文名字格式好看的_cf好看的英文名字格式
  15. z变换判断稳定性和因果性_判断因果性.PPT
  16. 计算机网络之简单概念
  17. android 是否插入耳机,Android监听耳机是否插入
  18. STL之容器Vector内存管理
  19. ARCore之路-技术原理(一)
  20. 百度之星2021 决赛

热门文章

  1. 论企业组织架构的扁平化管理
  2. STM32——FLASH擦除/写入失败的踩坑笔记。(WRPERR)
  3. 新款「超大杯」iPhone遭爆料!不止大镜头,还有1TB储存,但却不能叫iPhone13
  4. 玩转f#的一个实例——解拼图游戏
  5. 重塑汽车产业价值链,ChinaJoy诚邀造车新势力加盟
  6. psd格式图片一键切图
  7. 【Set】01-set参数
  8. VsCode常用插件和快捷键
  9. 哪款蓝牙耳机音质好?内行推荐四款高音质蓝牙耳机
  10. Qt for linux 安装