Chasing an mobile web vision

闯荡在移动互联网的世界中

OSGi介绍(五)两个bundle

(四)中提到的直接型改造法实际上和一个传统的java应用程序没有区别。因此客户的需求发生变化,通常是牵一发而动全身。
那么我们现在就看看如果在osgi framework中,用多个bundle来实现的效果吧。
我的想法是用两个bundle来配合实现“扶贫助手”的功能。一个bundle专门负责录入和显示纪录,一个bundle专门负责纪录的数据结构和对数据的处理,用时下时髦的说法就是使用了mvc,只是我的m和c放到了一起。
先看看mc的bundle实现代码片断:

package com.wukong.test.family.control;
import java.util.*;
import com.wukong.test.family.control.impl.*;
public interface FamilyInfoDatabase {
    
public static class FamilyFactory {
        
private static FamilyInfoDatabase database;
        
public static FamilyInfoDatabase getDatabaseInstance(){
            
if(database == null){
                database 
= new FamilyDatabase();
            }

            
return database;
        }

    }

    
public String[] getColumns();
    
public Object getValueAt(int row, int column);
    
public String[] getSortingFields();
    
public int getRowCount();
    
    
public void sort(String sortField) throws IllegalArgumentException;
    
    
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;
}



这个interface是用来描述一个数据库所具备的基本功能。注意,这个interface被放到了package com.wukong.test.family.control中。
另外,这个interface还提供了一个内嵌类,用来专门提供一个工厂方法来产生唯一的一个数据库实例。
对于这个数据库的实现,代码片断如下(增加,删除和修改的代码都被省略了,只给出排序的代码):
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(12));
    
private JLabel sortedByLabel = new JLabel("Sorted By: ");
    
private JComboBox sortedByList = null;
    
private JPanel commandPanel = new JPanel(new GridLayout(13));
    
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(500600));
                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就像动态连接库,只提供接口和方法以及实现,等待其他实体的调用,而本身不能运行。
再来看看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的代码,使得数据库的实现中增加这个字段以及相应的排序方法即可。更新后的代码如下:

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,有兴趣参与吗?不管怎样,过两天就把实现上传上来,欢迎拍砖。

posted on 2006-02-14 16:02 勤劳的蜜蜂 阅读(4270) 评论(3)  编辑  收藏

评论

# re: OSGi介绍(五)两个bundle 2007-06-22 10:27 hata

这章讲的比较长,要耐心看,有点遗憾的是没有把工具放上来我测试不鸟。在eclipse里的equinox插件里只能看到bundle启动与否的提示,看不到运行结果,很是郁闷。希望快点把实现传上来啊!
我分析下我的看法,看对不对啊:
我有2个bundle:bundleA & bundleB
bundleA里我写入: interface,pojo,iterfaceImpl
(在manifese里:Export-Package对外提供接口)
bundleB里我写入: 一系列的业务方法.
(在manifese里:Import-Package获取bundleA的服务)
那么这个时候就能运用到OSGI的功能了?我即时更新bundleA的话那么bundleB会相应发生变化!这就完成了最基本的OSGI的功能?
===>
附录:针对OSGI实战上的例子,那么功能基本相同,只是扩展了bundleB,多写了2个bundleC,bundleD?只是里面用到了servlet和service,我想我要看看你下面写的才知道怎么应用到上面.估计理解完后调通例子也就能进一步了解了.
===>
我们做J2EE,只是想把接口放在OSGI里,经理也只是这么说,希望能用这个做吧....期待你更多的OSGI的资料,我想你这个blog也算国内讨论osgi少有的好地方,加油加油!  回复  更多评论   

# re: OSGi介绍(五)两个bundle[未登录] 2009-07-24 10:04 han

有点长。。现在也不干活了。。天天看OSGI。。呵呵  回复  更多评论   

# re: OSGi介绍(五)两个bundle[未登录] 2009-07-24 17:26 han

我想问一下:FamilyInfoEntry 这个类不放到mc bundle的manifest文件中吗?
这个要怎么处理?  回复  更多评论   


只有注册用户登录后才能发表评论。


网站导航: