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 主要功能实现分析
在主界面启动点击“搜索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 中,然后更新表格的显示。
在搜索结果中,选择需要下载的行,然后单击“添加任务”。就把需要下载的歌曲添加的下载任务表格。主要有下面的代码:
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()
在下载任务窗口中,选择需要启动的任务,单击“开始”按钮。为了演示多线程并发和使用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();
在主界面选择需要暂停的任务,单击“暂停”就可以暂停已经运行的任务。暂停的基本原来就是阻塞线程,使其阻塞、进入等待队列。
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");
}
}
}
单击“停止”按钮后,会调用下载任务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;
}
}
单击“删除”按钮,执行下面的代码
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);
}
}
已经完成的任务需要从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) {
}
}
}
}
在为文件名生成序号的时候,使用了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目前虽然不是很完善,但是已经初露锋芒。
当然,本案例还有一些不完善的地方,比如不支持对同一个文件的分块下载。感兴趣的读者可以自己完善。