(四)中提到的直接型改造法實際上和一個傳統的java應用程序沒有區別。因此客戶的需求發生變化,通常是牽一發而動全身。
那么我們現在就看看如果在osgi framework中,用多個bundle來實現的效果吧。
我的想法是用兩個bundle來配合實現“扶貧助手”的功能。一個bundle專門負責錄入和顯示紀錄,一個bundle專門負責紀錄的數據結構和對數據的處理,用時下時髦的說法就是使用了mvc,只是我的m和c放到了一起。
先看看mc的bundle實現代碼片斷:
對于這個數據庫的實現,代碼片斷如下(增加,刪除和修改的代碼都被省略了,只給出排序的代碼):
package com.wukong.test.family.model.impl;

public class FamilyInfoEntry
{
private String familyName;
private int population;
private int incomePerYear;

FamilyInfoEntry(String familyName,int population,int income)
{
this.familyName = familyName;
this.population = population;
this.incomePerYear = income;
}

String getFamilyName()
{
return familyName;
}

int getIncomePerYear()
{
return incomePerYear;
}

int getPopulation()
{
return population;
}
}

package com.wukong.test.family.control.impl;
import com.wukong.test.family.control.FamilyInfoDatabase;
import java.util.*;

public class FamilyDatabase implements FamilyInfoDatabase
{
private LinkedList familyEntryList = new LinkedList();
private Object[] sortedValues = null;

public FamilyDatabase()
{
this.familyEntryList.add(new FamilyInfoEntry("Zhang",3,1200));
this.familyEntryList.add(new FamilyInfoEntry("Li",6,1800));
this.familyEntryList.add(new FamilyInfoEntry("Liu",4,1500));
this.sortedValues = this.familyEntryList.toArray();
}

public String[] getColumns()
{

return new String[]
{ "Family Name", "Family Population", "Income" };
}

public Object getValueAt(int row, int column)
{
FamilyInfoEntry entry = (FamilyInfoEntry)this.sortedValues[row];

switch (column)
{
case 0:
return entry.getFamilyName();
case 1:
return new Integer(entry.getPopulation());
case 2:
return new Integer(entry.getIncomePerYear());
default:
throw new IllegalArgumentException("Invalid column index.");
}
}

public String[] getSortingFields()
{

return new String[]
{ "FamilyName","Income" };
}

public int getRowCount()
{
return this.familyEntryList.size();
}
public void addEntry(List columns, List values)

throws IllegalArgumentException
{
}

public void deleteEntry(String familyName)
{
}
public void update(String familyName, List columns, List values)

throws IllegalArgumentException
{
}


public void sort(String sortField) throws IllegalArgumentException
{

if(sortField.equals("FamilyName"))
{
this.sortedValues = this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,new SortedByName());
}

if(sortField.equals("Income"))
{
this.sortedValues = this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,new SortedByIncome());
}
throw new IllegalArgumentException("Sorting enties by field '" + sortField + "' is not supported.");
}

class SortedByName implements Comparator
{

public int compare(Object entry1,Object entry2)
{

if (entry1 == entry2)
{
return 0;
}
FamilyInfoEntry en1 = (FamilyInfoEntry) entry1;
FamilyInfoEntry en2 = (FamilyInfoEntry) entry2;
return en1.getFamilyName().compareTo(en2.getFamilyName());
}
}

class SortedByIncome implements Comparator
{

public int compare(Object entry1, Object entry2)
{

if (entry1 == entry2)
{
return 0;
}
FamilyInfoEntry en1 = (FamilyInfoEntry) entry1;
FamilyInfoEntry en2 = (FamilyInfoEntry) entry2;
return en1.getIncomePerYear() - en2.getIncomePerYear();
}
}
}

同樣需要注意的是我們把這個實現放到了package com.wukong.test.family.control.impl中
下面看看v的bundle實現。
package com.wukong.test.family.gui;
import org.osgi.framework.*;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import com.wukong.test.family.control.*;

public class FamilyInfoGui implements BundleActivator, ActionListener,ItemListener
{
private JFrame mainFrame;
private JPanel contentPanel;
private JTable familiesTable;
private JScrollPane familiesTableScrollPane;
private JPanel sortedByPanel = new JPanel(new GridLayout(1, 2));
private JLabel sortedByLabel = new JLabel("Sorted By: ");
private JComboBox sortedByList = null;
private JPanel commandPanel = new JPanel(new GridLayout(1, 3));
private JButton addEntry = new JButton("Add");
private JButton deleteEntry = new JButton("Delete");
private JButton updateEntry = new JButton("Update");
FamilyInfoDatabase database = null;

public void start(BundleContext context) throws Exception
{

Runnable r = new Runnable()
{

public void run()
{
contentPanel = new JPanel();
familiesTableScrollPane = new JScrollPane();
database = FamilyInfoDatabase.FamilyFactory
.getDatabaseInstance();
familiesTable = new JTable(new FamilyInfoTableModel(database));
familiesTableScrollPane.setViewportView(familiesTable);
String[] sortedFields = database.getSortingFields();
sortedByList = new JComboBox(sortedFields);
sortedByList.addItemListener(FamilyInfoGui.this);
sortedByPanel.add(sortedByLabel);
sortedByPanel.add(sortedByList);
commandPanel.add(addEntry);
commandPanel.add(deleteEntry);
commandPanel.add(updateEntry);
contentPanel.add(sortedByPanel, BorderLayout.NORTH);
contentPanel.add(familiesTableScrollPane, BorderLayout.CENTER);
contentPanel.add(commandPanel, BorderLayout.SOUTH);
mainFrame = new JFrame();
mainFrame.setContentPane(contentPanel);
mainFrame.setSize(new Dimension(500, 600));
mainFrame.show();
}
};
Thread t = new Thread(r);
t.start();
}

public void stop(BundleContext context) throws Exception
{
this.mainFrame.dispose();
}

public void actionPerformed(ActionEvent event)
{
}

public void itemStateChanged(ItemEvent event)
{

if(event.getSource() == this.sortedByList)
{
String sortingField = (String)event.getItem();
this.database.sort(sortingField);
this.familiesTable.repaint();
}
}

class FamilyInfoTableModel extends AbstractTableModel
{
private FamilyInfoDatabase database;

FamilyInfoTableModel(FamilyInfoDatabase database)
{
this.database = database;
}

public String getColumnName(int col)
{
return database.getColumns()[col];
}

public boolean isCellEditable(int row, int col)
{
return false;
}

public int getColumnCount()
{
return database.getColumns().length;
}

public int getRowCount()
{
return database.getRowCount();
}

public Object getValueAt(int row, int col)
{
return database.getValueAt(row, col);
}
}
}v的實現基本是界面的構造,而關于數據庫,它只能看到interface FamilyInfoDatabase,因為它只import com.wukong.test.family.control.*,然后通過FamilyInfoDatabase內嵌靜態類的靜態工廠方法獲得數據庫的實例,這樣它完全不用關心數據庫如何實現。
源碼編譯后得到class文件后,下一步我們的工作就要來構造這兩個bundle。最關鍵的步驟就是為每個bundle寫它的manifest文件
我們先給出mc bundle的manifest文件(familycontrol.mf):
Manifest-Version: 1.0
Bundle-SymbolicName: com.wukong.test.family.control
Bundle-Name: family control
Bundle-Version: 1.0
Bundle-Vendor: LiMing
Export-Package: com.wukong.test.family.control

非常簡單,但請大家注意這行
Export-Package: com.wukong.test.family.control
它告訴framework,這個bundle提供com.wukong.test.family.control這個包。
所謂“bundle A提供某個包”意思,通俗簡單的理解是如果bundle B在manifest文件中說明要使用這個包(通過Import-Package這個關鍵字,下面會在v bundle中有說明)那么bundle B運行時,當使用到這個包中的類時,framework將告訴B的class loader這個類的定義是來自bundle A,從而不會發生ClassNotFoundException。另外值得一提的是我們并沒有把com.wukong.test.family.control.impl這個包加進來,這樣做的目的也是為了隱藏具體實現。將來只要interface不變,我們可以使用不同的實現來替換現有實現,大大提高擴展性。
還有一點,細心的話,你會發現這個bundle沒有activator。這是允許的,這種bundle就像動態連接庫,只提供接口和方法以及實現,等待其他實體的調用,而本身不能運行。
有了manifest文件,我們最后就是打jar包了。
比如在windows下,你把兩個manifest文件放在包的根目錄,然后執行以下命令
jar -cvfm family.jar family.mf com\wukong\test\family\gui\*.class
jar -cvfm familycontrol.jar familycontrol.mf com\wukong\test\family\control
這樣,我們就可以認為family.jar是我們的v bundle而familycontrol.jar是我們的mc bundle。
下面,我們就來安裝運行這兩個bundle吧。
我將首先簡單介紹一個我自己編寫的osgi framework,然后結合這個demo framework來部署和運行新的“扶貧助手”。
本人編寫的osgi framework基本上完成了spec r4的核心規范,但是與安全相關的功能都沒有實現。
啟動這個framework后,它的shell提供了如下簡單的framework管理功能命令:
bl 即bundle list,查看所有已經安裝的bundle
dl 即detailed bundle list,用來查看某個bundle的詳細信息
sl 即service list,用來查看所有已經注冊的service
ls 即list setting,用來查看所有設置的系統屬性(System.getProperties)和framework當前的active start level
in 即install bundle,通過指定bundle文件的url來安裝該bundle
up 即update bundle,通過指定bundle id來升級指定的bundle
un 即uninstall bundle,通過指定bundle id來卸載指定的bundle
stt 即start bundle,通過指定bundle id來啟動指定的bundle
stp 即stop bundle,通過指定bundle id來停止指定bundle的運行
rst 即restart bundle,通過指定bundle id來重新啟動指定的bundle
rfr 即refresh package,當某些bundle被更新或卸載后,如果不執行這個命令,那么被更新或卸載的bundle的Export-Package(如果有的話)將繼續服役,
也就是說,之前使用這些包的bundle不會因為這個bundle被更新或者卸載而發生變化。而執行這個命令后,受影響的bundle將被重新解析
rest 即restart,重新啟動framework,屬于“熱起”
shut 即shutdown,停止整個framework的運行
set 用來設置framework的active startlevel或者某個bundle的start level
log 用來查看系統的日志信息
現在我們來假設運行環境如下:
OS: window
jvm: j2se 1.4.2_04
family.jar和familycontrol.jar這兩個文件放在D:/framework/bundles目錄下
第一步:安裝bundle
輸入以下兩條命令:
in file:D:/framework/bundles/familycontrol.jar
in file:D:/framework/bundles/family.jar
然后用bl命令查看結果,其中得到如下顯示結果:
Bundle Id Bundle Name Bundle State Start Level Bundle Location Bundle Symbolic Name
0 OSGiFramework Active 0 System Bundle system.bundle
.
.(省略部分結果顯示)
.
16 family control Installed 1 file:D:/framework/bundles/familycontrol.jar com.wukong.test.family.control
17 family gui Installed 1 file:D:/framework/bundles/family.jar com.wukong.test.family.gui
說明兩個bundle安裝成功,其中mc bundle的id是16, v bundle的id是17
第二步:啟動bundle
可以輸入以下命令
stt 16,17
然后我們就可以看到一個JFrame顯示出來,里面顯示了3條記錄,沒有任何順序(其順序實際上是在程序中添加到LinkedList的順序)。我們可以在Sorted By的列表中看到有兩個選擇“FamilyName”和“Income”,當選擇“Income”時,你將發現3條記錄按年收入由小到大排列起來。程序的運行完全符合我們的意圖。
這時如果我們再用bl命令查看,將得到如下結果:
Bundle Id Bundle Name Bundle State Start Level Bundle Location Bundle Symbolic Name
0 OSGiFramework Active 0 System Bundle system.bundle
.
.(省略部分結果顯示)
.
16 family control Resolved 1 file:D:/framework/bundles/familycontrol.jar com.wukong.test.family.control
17 family gui Active 1 file:D:/framework/bundles/family.jar com.wukong.test.family.gui
注意:由于mc bundle沒有Activator,所以啟動這個bundle只會使其進入Resolved狀態。
有了這樣的結構,程序變得靈活些了。現在假設客戶提出需要增加一個字段,即人均年收入,并且提供按人均年收入排列紀錄。看看framework框架下,這樣的變動是如何的簡單!
首先,我們通過分析,發現只要更改mc bundle的代碼,使得數據庫的實現中增加這個字段以及相應的排序方法即可。更新后的代碼如下:
package com.wukong.test.family.control.impl;
import com.wukong.test.family.control.FamilyInfoDatabase;
import java.util.*;

public class FamilyDatabase implements FamilyInfoDatabase
{
private LinkedList familyEntryList = new LinkedList();
private Object[] sortedValues = null;

public FamilyDatabase()
{
this.familyEntryList.add(new FamilyInfoEntry("Zhang",3,1260));
this.familyEntryList.add(new FamilyInfoEntry("Li",6,1800));
this.familyEntryList.add(new FamilyInfoEntry("Liu",4,1500));
this.sortedValues = this.familyEntryList.toArray();
}

public String[] getColumns()
{

return new String[]
{ "Family Name", "Family Population", "Income" ,"IncomePerPerson"};//添加了“IncomePerPerson”字段
}

public Object getValueAt(int row, int column)
{
FamilyInfoEntry entry = (FamilyInfoEntry)this.sortedValues[row];

switch (column)
{
case 0:
return entry.getFamilyName();
case 1:
return new Integer(entry.getPopulation());
case 2:
return new Integer(entry.getIncomePerYear());
case 3:
return new Integer(entry.getIncomePerYear()/entry.getPopulation());//計算人均年收入
default:
throw new IllegalArgumentException("Invalid column index.");
}
}

public String[] getSortingFields()
{
}

public int getRowCount()
{
return this.familyEntryList.size();
}
public void addEntry(List columns, List values)

throws IllegalArgumentException
{
// TODO Auto-generated method stub
}

public void deleteEntry(String familyName)
{
// TODO Auto-generated method stub
}
public void update(String familyName, List columns, List values)

throws IllegalArgumentException
{
// TODO Auto-generated method stub
}

public void sort(String sortField) throws IllegalArgumentException
{

if(sortField.equals("FamilyName"))
{
this.sortedValues = this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,new SortedByName());
}

if(sortField.equals("Income"))
{
this.sortedValues = this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,new SortedByIncome());
}

if(sortField.equals("IncomePerPerson"))
{//對紀錄按人均年收入進行排序
this.sortedValues = this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,new SortedByIPP());
}
}

class SortedByName implements Comparator
{

public int compare(Object entry1,Object entry2)
{

if (entry1 == entry2)
{
return 0;
}
FamilyInfoEntry en1 = (FamilyInfoEntry) entry1;
FamilyInfoEntry en2 = (FamilyInfoEntry) entry2;
return en1.getFamilyName().compareTo(en2.getFamilyName());
}
}

class SortedByIncome implements Comparator
{

public int compare(Object entry1, Object entry2)
{

if (entry1 == entry2)
{
return 0;
}
FamilyInfoEntry en1 = (FamilyInfoEntry) entry1;
FamilyInfoEntry en2 = (FamilyInfoEntry) entry2;
return en1.getIncomePerYear() - en2.getIncomePerYear();
}
}

class SortedByIPP implements Comparator
{//人均年收入的排序標準。

public int compare(Object entry1, Object entry2)
{

if (entry1 == entry2)
{
return 0;
}
FamilyInfoEntry en1 = (FamilyInfoEntry) entry1;
FamilyInfoEntry en2 = (FamilyInfoEntry) entry2;
return (en1.getIncomePerYear()/en1.getPopulation()) - (en2.getIncomePerYear()/en2.getPopulation());
}
}
}

編譯完畢后,我們把mc bundle的manifest文件改為如下:
Manifest-Version: 1.0
Bundle-SymbolicName: com.wukong.test.family.control
Bundle-Name: family control
Bundle-Version: 2.0 //由1.0改為2.0
Bundle-Vendor: LiMing
Export-Package: com.wukong.test.family.control

然后使用jar工具打包,并把產生的新的familycontrol.jar依舊放在D:/framework/bundles目錄下。
這時,我們只要執行下面的命令,framework就完成對mc bundle的升級工作:
up 16
升級完畢后,你會發現v bundle的顯示依舊沒有任何變化。這時我們需要執行一個refresh package命令,來更新bundle的解析:
rfr
這時,你會發現原來的JFrame被關閉了,然后又一個JFrame顯示出來,里面的紀錄多了一列“IncomePerPerson”,而且在“Sorted By”里面也
多了“IncomePerPerson”,而且,排序的結果也很正確。
瞧,framework為應用程序提供了強大的動態更新功能,非常方便。甚至都不用重新啟動framework,更不用說JVM了。您也許會說,最終用戶根本不可能幫你輸入這些什么in,up晦澀的命令。還有,讓用戶拷貝新版本到本地似乎也不是什么好主意。嗯,這個問題說得很對。其實我是偷懶了,這些操作framework都留有接口,我們只要再編寫一個專門管理這兩個bundle升級的第三個bundle就可以了,這個bundle將提供友好的界面來告訴最終用戶如何升級。這樣就能夠提供比較好的解決方案了。至于第二個問題,別忘了bundle的location是一個url,只要jvm注冊了相應的url handler完全可以支持http,ftp等豐富的url,例如,升級的時候,framework自動連接到供應商的http下載服務器獲取最新版本進行更新,當然,我們也可以把更強大更靈活的升級功能及選項加入到上面提到的第三個bundle中,從而極大提高了程序的易用性,而且極大降低了軟件提供商的維護成本。
到此為止,我已經想不出再多好話了,但是,還可以更好!下一節,將采用service的方式來實現,嘿嘿,情況又會不一樣了,到時候我會對比一下這兩種實現方式的優劣。
題外話:抱歉沒有拿某個著名的產品或者open source的產品來舉例,我打算把自己的實現也opensource,有興趣參與嗎?不管怎樣,過兩天就把實現上傳上來,歡迎拍磚。