(四)中提到的直接型改造法实际上和一个传统的java应用程序没有区别。因此客户的需求发生变化,通常是牵一发而动全身。
那么我们现在就看看如果在osgi framework中,用多个bundle来实现的效果吧。
我的想法是用两个bundle来配合实现“扶贫助手”的功能。一个bundle专门负责录入和显示纪录,一个bundle专门负责纪录的数据结构和对数据的处理,用时下时髦的说法就是使用了mvc,只是我的m和c放到了一起。
先看看mc的bundle实现代码片断:
packagecom.wukong.test.family.control;
importjava.util.*;
importcom.wukong.test.family.control.impl.*;
publicinterfaceFamilyInfoDatabase
{
publicstaticclassFamilyFactory
{
privatestaticFamilyInfoDatabase database;
publicstaticFamilyInfoDatabase getDatabaseInstance()
{
if(database==null)
{
database=newFamilyDatabase();
}
returndatabase;
}
}
publicString[] getColumns();
publicObject getValueAt(introw,intcolumn);
publicString[] getSortingFields();
publicintgetRowCount();
publicvoidsort(String sortField)throwsIllegalArgumentException;
publicvoidaddEntry(List columns, List values)throwsIllegalArgumentException;
publicvoiddeleteEntry(String familyName);
publicvoidupdate(String familyName,List columns, List values)throwsIllegalArgumentException;
}
这个interface是用来描述一个数据库所具备的基本功能。注意,这个interface被放到了package com.wukong.test.family.control中。
另外,这个interface还提供了一个内嵌类,用来专门提供一个工厂方法来产生唯一的一个数据库实例。
对于这个数据库的实现,代码片断如下(增加,删除和修改的代码都被省略了,只给出排序的代码):
packagecom.wukong.test.family.model.impl;
publicclassFamilyInfoEntry
{
privateString familyName;
privateintpopulation;
privateintincomePerYear;
FamilyInfoEntry(String familyName,intpopulation,intincome)
{
this.familyName=familyName;
this.population=population;
this.incomePerYear=income;
}
String getFamilyName()
{
returnfamilyName;
}
intgetIncomePerYear()
{
returnincomePerYear;
}
intgetPopulation()
{
returnpopulation;
}
}
packagecom.wukong.test.family.control.impl;
importcom.wukong.test.family.control.FamilyInfoDatabase;
importjava.util.*;
publicclassFamilyDatabaseimplementsFamilyInfoDatabase
{
privateLinkedList familyEntryList=newLinkedList();
privateObject[] sortedValues=null;
publicFamilyDatabase()
{
this.familyEntryList.add(newFamilyInfoEntry("Zhang",3,1200));
this.familyEntryList.add(newFamilyInfoEntry("Li",6,1800));
this.familyEntryList.add(newFamilyInfoEntry("Liu",4,1500));
this.sortedValues=this.familyEntryList.toArray();
}
publicString[] getColumns()
{
returnnewString[]
{"Family Name","Family Population","Income"};
}
publicObject getValueAt(introw,intcolumn)
{
FamilyInfoEntry entry=(FamilyInfoEntry)this.sortedValues[row];
switch(column)
{
case0:
returnentry.getFamilyName();
case1:
returnnewInteger(entry.getPopulation());
case2:
returnnewInteger(entry.getIncomePerYear());
default:
thrownewIllegalArgumentException("Invalid column index.");
}
}
publicString[] getSortingFields()
{
returnnewString[]
{"FamilyName","Income"};
}
publicintgetRowCount()
{
returnthis.familyEntryList.size();
}
publicvoidaddEntry(List columns, List values)
throwsIllegalArgumentException
{
}
publicvoiddeleteEntry(String familyName)
{
}
publicvoidupdate(String familyName, List columns, List values)
throwsIllegalArgumentException
{
}
publicvoidsort(String sortField)throwsIllegalArgumentException
{
if(sortField.equals("FamilyName"))
{
this.sortedValues=this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,newSortedByName());
}
if(sortField.equals("Income"))
{
this.sortedValues=this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,newSortedByIncome());
}
thrownewIllegalArgumentException("Sorting enties by field '"+sortField+"' is not supported.");
}
classSortedByNameimplementsComparator
{
publicintcompare(Object entry1,Object entry2)
{
if(entry1==entry2)
{
return0;
}
FamilyInfoEntry en1=(FamilyInfoEntry) entry1;
FamilyInfoEntry en2=(FamilyInfoEntry) entry2;
returnen1.getFamilyName().compareTo(en2.getFamilyName());
}
}
classSortedByIncomeimplementsComparator
{
publicintcompare(Object entry1, Object entry2)
{
if(entry1==entry2)
{
return0;
}
FamilyInfoEntry en1=(FamilyInfoEntry) entry1;
FamilyInfoEntry en2=(FamilyInfoEntry) entry2;
returnen1.getIncomePerYear()-en2.getIncomePerYear();
}
}
}
同样需要注意的是我们把这个实现放到了package com.wukong.test.family.control.impl中
下面看看v的bundle实现。
packagecom.wukong.test.family.gui;
importorg.osgi.framework.*;
importjavax.swing.*;
importjavax.swing.table.*;
importjava.awt.*;
importjava.awt.event.*;
importcom.wukong.test.family.control.*;
publicclassFamilyInfoGuiimplementsBundleActivator, ActionListener,ItemListener
{
privateJFrame mainFrame;
privateJPanel contentPanel;
privateJTable familiesTable;
privateJScrollPane familiesTableScrollPane;
privateJPanel sortedByPanel=newJPanel(newGridLayout(1,2));
privateJLabel sortedByLabel=newJLabel("Sorted By:");
privateJComboBox sortedByList=null;
privateJPanel commandPanel=newJPanel(newGridLayout(1,3));
privateJButton addEntry=newJButton("Add");
privateJButton deleteEntry=newJButton("Delete");
privateJButton updateEntry=newJButton("Update");
FamilyInfoDatabase database=null;
publicvoidstart(BundleContext context)throwsException
{
Runnable r=newRunnable()
{
publicvoidrun()
{
contentPanel=newJPanel();
familiesTableScrollPane=newJScrollPane();
database=FamilyInfoDatabase.FamilyFactory
.getDatabaseInstance();
familiesTable=newJTable(newFamilyInfoTableModel(database));
familiesTableScrollPane.setViewportView(familiesTable);
String[] sortedFields=database.getSortingFields();
sortedByList=newJComboBox(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=newJFrame();
mainFrame.setContentPane(contentPanel);
mainFrame.setSize(newDimension(500,600));
mainFrame.show();
}
};
Thread t=newThread(r);
t.start();
}
publicvoidstop(BundleContext context)throwsException
{
this.mainFrame.dispose();
}
publicvoidactionPerformed(ActionEvent event)
{
}
publicvoiditemStateChanged(ItemEvent event)
{
if(event.getSource()==this.sortedByList)
{
String sortingField=(String)event.getItem();
this.database.sort(sortingField);
this.familiesTable.repaint();
}
}
classFamilyInfoTableModelextendsAbstractTableModel
{
privateFamilyInfoDatabase database;
FamilyInfoTableModel(FamilyInfoDatabase database)
{
this.database=database;
}
publicString getColumnName(intcol)
{
returndatabase.getColumns()[col];
}
publicbooleanisCellEditable(introw,intcol)
{
returnfalse;
}
publicintgetColumnCount()
{
returndatabase.getColumns().length;
}
publicintgetRowCount()
{
returndatabase.getRowCount();
}
publicObject getValueAt(introw,intcol)
{
returndatabase.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就像动态连接库,只提供接口和方法以及实现,等待其他实体的调用,而本身不能运行。
再来看看v bundle的manifest文件(family.mf):
Manifest-Version:1.0
Bundle-SymbolicName: com.wukong.test.family.gui
Bundle-Name: family gui
Bundle-Version:1.0
Bundle-Vendor: LiMing
Bundle-Activator: com.wukong.test.family.gui.FamilyInfoGui
Import-Package: org.osgi.framework;version=1.3,com.wukong.test.family.control
正如上面曾经提到过的一样,我们在Import-Package:关键字中指定该bundle需要使用com.wukong.test.family.control包。在v bundle的源代码中,我们也看到这个bundle的确使用了com.wukong.test.family.control包中的FamilyInfoDatabase。更深入一点,我们可以简单的这样理解,当v bundle运行的时候,framework通过匹配bundle间的Import-Package和Export-Package,
然后告诉v bundle,FamilyInfoDatabase这个类型的定义和装载实际上是由mc bundle完成的。这样,v bundle就会使用mc bundle的class loader来加在这个类型。另外,按照osgi framework的spec规定,javax.swing;javax.swing.table;java.awt;java.awt.event这4个包都都不属于framework的必备包,所以应该在Import-Package中指定。为了不用在Import-Package中指定这4个包,这个例子要求运行在sun的j2se的jvm(比如version 1.4.2_04),这样4个包都作为java api由bootstrap classloader加载,每个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的代码,使得数据库的实现中增加这个字段以及相应的排序方法即可。更新后的代码如下:
packagecom.wukong.test.family.control.impl;
importcom.wukong.test.family.control.FamilyInfoDatabase;
importjava.util.*;
publicclassFamilyDatabaseimplementsFamilyInfoDatabase
{
privateLinkedList familyEntryList=newLinkedList();
privateObject[] sortedValues=null;
publicFamilyDatabase()
{
this.familyEntryList.add(newFamilyInfoEntry("Zhang",3,1260));
this.familyEntryList.add(newFamilyInfoEntry("Li",6,1800));
this.familyEntryList.add(newFamilyInfoEntry("Liu",4,1500));
this.sortedValues=this.familyEntryList.toArray();
}
publicString[] getColumns()
{
returnnewString[]
{"Family Name","Family Population","Income","IncomePerPerson"};//添加了“IncomePerPerson”字段
}
publicObject getValueAt(introw,intcolumn)
{
FamilyInfoEntry entry=(FamilyInfoEntry)this.sortedValues[row];
switch(column)
{
case0:
returnentry.getFamilyName();
case1:
returnnewInteger(entry.getPopulation());
case2:
returnnewInteger(entry.getIncomePerYear());
case3:
returnnewInteger(entry.getIncomePerYear()/entry.getPopulation());//计算人均年收入
default:
thrownewIllegalArgumentException("Invalid column index.");
}
}
publicString[] getSortingFields()
{
}
publicintgetRowCount()
{
returnthis.familyEntryList.size();
}
publicvoidaddEntry(List columns, List values)
throwsIllegalArgumentException
{
//TODO Auto-generated method stub
}
publicvoiddeleteEntry(String familyName)
{
//TODO Auto-generated method stub
}
publicvoidupdate(String familyName, List columns, List values)
throwsIllegalArgumentException
{
//TODO Auto-generated method stub
}
publicvoidsort(String sortField)throwsIllegalArgumentException
{
if(sortField.equals("FamilyName"))
{
this.sortedValues=this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,newSortedByName());
}
if(sortField.equals("Income"))
{
this.sortedValues=this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,newSortedByIncome());
}
if(sortField.equals("IncomePerPerson"))
{//对纪录按人均年收入进行排序
this.sortedValues=this.familyEntryList.toArray();
Arrays.sort(this.sortedValues,newSortedByIPP());
}
}
classSortedByNameimplementsComparator
{
publicintcompare(Object entry1,Object entry2)
{
if(entry1==entry2)
{
return0;
}
FamilyInfoEntry en1=(FamilyInfoEntry) entry1;
FamilyInfoEntry en2=(FamilyInfoEntry) entry2;
returnen1.getFamilyName().compareTo(en2.getFamilyName());
}
}
classSortedByIncomeimplementsComparator
{
publicintcompare(Object entry1, Object entry2)
{
if(entry1==entry2)
{
return0;
}
FamilyInfoEntry en1=(FamilyInfoEntry) entry1;
FamilyInfoEntry en2=(FamilyInfoEntry) entry2;
returnen1.getIncomePerYear()-en2.getIncomePerYear();
}
}
classSortedByIPPimplementsComparator
{//人均年收入的排序标准。
publicintcompare(Object entry1, Object entry2)
{
if(entry1==entry2)
{
return0;
}
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,有兴趣参与吗?不管怎样,过两天就把实现上传上来,欢迎拍砖。
posted on 2006-02-14 16:02 勤劳的蜜蜂 阅读(4288) 评论(3) 编辑 收藏