Chasing an mobile web vision

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

OSGi介绍(六)OSGi的service

在给出采用service方式实现的“扶贫助手”之前,我们稍微回顾一下上一篇的成果。
在(五)中,我们看到程序被分成多个bundle后,程序的模块程度得到提高,而控制模块间的耦合度由Import-Package和Export-Package来控制,相对比较灵活。另一方面程序的更新和升级的粒度变小了。谁都知道只更新部分要比全部更新强,尤其当更新发生在一些需要建立昂贵的连接时,细粒度会节省不少花销。除了这些,我们看不到其他新鲜的东西。说白了,也就是挖空心思想一些design pattern来划分程序模块。
 
好了,马上就新鲜了。下面你会看到通过采用service方式来改造(五)中的程序,gui bundle在某些情况下不用重新启动,就能直接某些适应需求的变更!
先给出model bundle的代码,该bundle包含两个java package,分别是:
com.bajie.test.family.model
com.bajie.test.family.model.impl
在com.bajie.test.family.model这个package中包含如下的class和interface:
package com.bajie.test.family.model;
import java.util.List;
import javax.swing.table.AbstractTableModel;
public abstract class FamilyInfoDatabase extends AbstractTableModel{
   
    public abstract void sort(SortingFamilyInfoCriteria sortField) throws IllegalArgumentException;
   
    public abstract void addEntry(List columns, List values) throws IllegalArgumentException;
    public abstract void deleteEntry(String familyName);
    public abstract void update(String familyName,List columns, List values)throws IllegalArgumentException;
}

这是database的model,与(五)定义成interface不同,我们直接让它继承了AbstractTableModel,这是因为我们希望当数据或显示需求变化时,gui上的JTable能获得通知,并显示更新的结果。SortingFamilyInfoCriteria这个类型下文会给出说明。
 
package com.bajie.test.family.model;
public class FamilyInfoEntry {
    private String familyName;
    private int population;
    private int incomePerYear;
   
    public FamilyInfoEntry(String familyName,int population,int income){
        this.familyName = familyName;
        this.population = population;
        this.incomePerYear = income;
    }
   
    public String getFamilyName() {
        return familyName;
    }
    public int getIncomePerYear() {
        return incomePerYear;
    }
    public int getPopulation() {
        return population;
    }
}

这个类的结构和在(五)中完全一样,用来纪录一条家庭信息。唯一不同的是,在(五)中我们把它放入了实现(.impl)package中,在后面给出bundle的manifest文件时,我将解释为什么要这样改。
 
package com.bajie.test.family.model;
public interface FamilyInfoColumn {
    public Object getColumnValue(FamilyInfoEntry entry);
   
    public String getColumnName();
}
这个类用来描述table中的某个列。
package com.bajie.test.family.model;
import java.util.Comparator;
public interface SortingFamilyInfoCriteria extends Comparator{
    public String getSortFieldString();
}
这个类将用于对家庭纪录按某一列的值进行排序。
在com.bajie.test.family.model.impl这个package中包含上面抽象类和interface的实现:
package com.bajie.test.family.model.impl;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import com.bajie.test.family.model.FamilyInfoColumn;
import com.bajie.test.family.model.FamilyInfoDatabase;
import com.bajie.test.family.model.FamilyInfoEntry;
import com.bajie.test.family.model.SortingFamilyInfoCriteria;
public class FamilyDatabase extends FamilyInfoDatabase implements  BundleActivator,
        ServiceListener {
    private LinkedList familyEntryList = new LinkedList();
    private Object[] sortedValues = null;
    private LinkedList columns = new LinkedList();
    private BundleContext context;
    public int getColumnCount() {
        return this.columns.size();
    }
    public String getColumnName(int index) {
        return ((FamilyInfoColumn)columns.get(index)).getColumnName();
    }
   
    public Object getValueAt(int row, int column) {
        FamilyInfoEntry entry = (FamilyInfoEntry) this.sortedValues[row];
        if(column >= this.familyEntryList.size()){
            return null;
        }
        return ((FamilyInfoColumn) this.columns.get(column))
                .getColumnValue(entry);
    }
    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(SortingFamilyInfoCriteria sortField) {
        Arrays.sort(this.sortedValues, sortField);
    }
    public void start(BundleContext context) throws Exception {
        this.context = context;
        this.familyEntryList.add(new FamilyInfoEntry("Zhang", 3, 1200));
        this.familyEntryList.add(new FamilyInfoEntry("Li", 6, 1800));
        this.familyEntryList.add(new FamilyInfoEntry("Liu", 5, 1500));
        this.familyEntryList.add(new FamilyInfoEntry("Wang", 4, 1300));
       
        this.sortedValues = this.familyEntryList.toArray();
 //向framework注册一个类型为FamilyInfoDatabase的服务
        context.registerService(FamilyInfoDatabase.class.getName(),this,null);
 //向framework注册三个服务,每个服务的类型既为FamilyInfoColumn,也是SortingFamilyInfoCriteria
        String[] clazzes = new String[] {FamilyInfoColumn.class.getName(),SortingFamilyInfoCriteria.class.getName()};
        context.registerService(clazzes,new FamilyNameColumn(),null);
        context.registerService(clazzes,new FamilyPopulationColumn(),null);
        context.registerService(clazzes,new FamilyIncomeColumn(),null);
       
 //向framework查找所有注册类型为FamilyInfoColumn的服务
 //先获得服务的引用
        ServiceReference[] columnRefs = context.getServiceReferences(
                FamilyInfoColumn.class.getName(), null);
        FamilyInfoColumn column = null;
        for (int i = 0; i < columnRefs.length; i++) {
            System.out.println(i + ":" + ((String[])(columnRefs[i].getProperty(Constants.OBJECTCLASS)))[0]);
     //通过引用获得具体的服务对象,每一个对象都将转化成gui中table的一列
            column = (FamilyInfoColumn) context.getService(columnRefs[i]);
            if (column != null) {
                this.columns.add(column);
            }else{
                System.out.println("null service object.");
            }
        }

 //注册服务侦听器,该侦听器专门侦听FamilyInfoColumn服务对象的动态(主要是增加和删除)
        context.addServiceListener(this,"(" + Constants.OBJECTCLASS + "="
                + FamilyInfoColumn.class.getName() + ")");
    }
    public void stop(BundleContext context) throws Exception {
    }
    public void serviceChanged(ServiceEvent event) {
        switch (event.getType()) {
        case ServiceEvent.MODIFIED:
            return;
        case ServiceEvent.REGISTERED://表明有新的列产生了。
            ServiceReference ref = event.getServiceReference();
            Object service = this.context.getService(ref);
            this.columns.add(service);
            this.fireTableStructureChanged();//通知gui,表结构发生变化
            return;
        case ServiceEvent.UNREGISTERING://表明有些列将被删除
            ref = event.getServiceReference();
            service = this.context.getService(ref);
            this.columns.remove(service);
            this.fireTableStructureChanged();//通知gui,表结构发生变化
            return;
        }
    }

    //这个类定义一个“Family Name”这个列,以及如何按这个列的值进行排序
    class FamilyNameColumn implements FamilyInfoColumn,SortingFamilyInfoCriteria {
        private static final String COLUMNNAME = "Family Name";
       
        public Object getColumnValue(FamilyInfoEntry entry) {
            return entry.getFamilyName();
        }
       
       
        public String getColumnName() {
            return FamilyNameColumn.COLUMNNAME;
        }
       
        public String getSortFieldString() {
            return FamilyNameColumn.COLUMNNAME;
        }
        public int compare(Object obj1, Object obj2) {
            if (obj1 == obj2) {
                return 0;
            }
            FamilyInfoEntry en1 = (FamilyInfoEntry)obj1;
            FamilyInfoEntry en2 = (FamilyInfoEntry)obj2;
           
            return en1.getFamilyName().compareTo(en2.getFamilyName());
        }
       
    }
    //这个类定义一个“Family Population”这个列,以及如何按这个列的值进行排序
    class FamilyPopulationColumn implements FamilyInfoColumn, SortingFamilyInfoCriteria {
        private static final String COLUMNNAME = "Family Population";
        public Object getColumnValue(FamilyInfoEntry entry) {
            return new Integer(entry.getPopulation());
        }
        public String getColumnName() {
            return FamilyPopulationColumn.COLUMNNAME;
        }
       
        public String getSortFieldString() {
            return FamilyPopulationColumn.COLUMNNAME;
        }
       
        public int compare(Object obj1, Object obj2) {
            if (obj1 == obj2) {
                return 0;
            }
            FamilyInfoEntry en1 = (FamilyInfoEntry)obj1;
            FamilyInfoEntry en2 = (FamilyInfoEntry)obj2;
           
            return en1.getPopulation() - en2.getPopulation();
        }
    }
   
    //这个类定义一个“Family Income”这个列,以及如何按这个列的值进行排序
    class FamilyIncomeColumn implements FamilyInfoColumn, SortingFamilyInfoCriteria {
        private static final String COLUMNNAME = "Family Income";
        public Object getColumnValue(FamilyInfoEntry entry) {
            return new Integer(entry.getIncomePerYear());
        }
        public String getColumnName() {
            return FamilyIncomeColumn.COLUMNNAME;
        }
       
       
        public String getSortFieldString() {
            return FamilyIncomeColumn.COLUMNNAME;
        }
        public int compare(Object obj1, Object obj2) {
            if (obj1 == obj2) {
                return 0;
            }
            FamilyInfoEntry en1 = (FamilyInfoEntry)obj1;
            FamilyInfoEntry en2 = (FamilyInfoEntry)obj2;
           
            return en1.getIncomePerYear() - en2.getIncomePerYear();
        }
       
    }
}
 
与(五)相比,最大的不同就是表结构的“列”是通过查找所有类型为FamilyInfoColumn的服务对象而组成的。而通过framework提供的服务侦听机制(即实现ServiceListener接口并注册到framework中),bundle能够获得该类服务对象的动态事件通知,如果该事件是新服务注册,则添加一个显示列,如果是服务被注销,则删除对应的显示列。
 
下面是bundle的manifest文件
Manifest-Version: 1.0
Bundle-SymbolicName: com.bajie.test.family.model
Bundle-Name: family model
Bundle-Version: 1.0
Bundle-Vendor: LiMing
Bundle-Activator: com.bajie.test.family.model.impl.FamilyDatabase
Import-Package: org.osgi.framework;version=1.3,com.bajie.test.family.model
Export-Package: com.bajie.test.family.model;version=1.0
从中我们看到com.bajie.test.family.model这个package被export出来,这样其他bundle就能够import这个package,并根据FamilyInfoEntry所提供的基本内容提供一些额外的处理结果,从而产生新列(FamilyInfoColumn)以及排序方法(SortingFamilyInfoCriteria),比如家庭人均年收入。

下面来看看gui bundle,它只包含一个package
package com.bajie.test.family.gui;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.Hashtable;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import com.bajie.test.family.model.FamilyInfoDatabase;
import com.bajie.test.family.model.SortingFamilyInfoCriteria;
public class FamilyInfoGui implements BundleActivator, ActionListener,
        ItemListener, ServiceListener {
    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");
    private Hashtable sortingFields = new Hashtable();
    private BundleContext context;
    FamilyInfoDatabase database = null;
    public void start(BundleContext context) throws Exception {
        this.context = context;
        //查找所有注册类型为FamilyInfoDatabase的服务对象。在我们这个例子,它是由上面给出的model bundle注册的
        ServiceReference databaseServiceRef = context
                .getServiceReference(FamilyInfoDatabase.class.getName());
        if (databaseServiceRef == null) {
            System.out.println("No database service is registered.");
            return;
        }
 //这个服务对象将成为JTable的数据model
        this.database = (FamilyInfoDatabase) context
                .getService(databaseServiceRef);
        if (this.database == null) {
            System.out.println("Can not get database object");
            return;
        }
        //查找所有注册类型为SortingFamilyInfoCriteria的服务对象。
        ServiceReference[] sortingCriteria = context.getServiceReferences(
                SortingFamilyInfoCriteria.class.getName(), null);
        sortedByList = new JComboBox();
        SortingFamilyInfoCriteria criterion = null;
        if (sortingCriteria != null) {
            for (int i = 0; i < sortingCriteria.length; i++) {
                criterion = (SortingFamilyInfoCriteria) context
                        .getService(sortingCriteria[i]);
                if (criterion != null) {
      //每个服务对象将对应一种排序方法,并加入到下拉列表中
                    sortedByList.addItem(criterion.getSortFieldString());
                    this.sortingFields.put(criterion.getSortFieldString(),
                            criterion);
                }
            }
        }
 //注册服务侦听器,该侦听器专门侦听SortingFamilyInfoCriteria服务对象的动态(主要是增加和删除)
        context.addServiceListener(this, "(" + Constants.OBJECTCLASS + "="
                + SortingFamilyInfoCriteria.class.getName() + ")");
        sortedByList.addItemListener(FamilyInfoGui.this);
        //construct gui
        Runnable r = new Runnable() {
            public void run() {
                contentPanel = new JPanel();
                familiesTableScrollPane = new JScrollPane();
  //获得的FamilyInfoDatabase对象成为gui中JTable的model
                familiesTable = new JTable(database);
                familiesTableScrollPane.setViewportView(familiesTable);
                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 {
        if (this.mainFrame != null)
            this.mainFrame.dispose();
    }
    public void actionPerformed(ActionEvent event) {
    }
    public void itemStateChanged(ItemEvent event) {
        if (event.getSource() == this.sortedByList) {
            SortingFamilyInfoCriteria criterion = (SortingFamilyInfoCriteria) this.sortingFields
                    .get(event.getItem());
            if (criterion == null)
                return;
            this.database.sort(criterion);
            this.familiesTable.repaint();
        }
    }
    public void serviceChanged(ServiceEvent event) {
        switch (event.getType()) {
        case ServiceEvent.MODIFIED:
            return;
        case ServiceEvent.REGISTERED://有新的排序方法注册到framework当中
            ServiceReference ref = event.getServiceReference();
            SortingFamilyInfoCriteria criterion = (SortingFamilyInfoCriteria) this.context
                    .getService(ref);
            if (criterion != null) {
  //把新的排序方法加入到下拉列表中
                sortedByList.addItem(criterion.getSortFieldString());
                this.sortingFields.put(criterion.getSortFieldString(),
                        criterion);
            }
            return;
        case ServiceEvent.UNREGISTERING://一个现有的排序方法将被从framework被取消
            ref = event.getServiceReference();
            criterion = (SortingFamilyInfoCriteria) this.context
                    .getService(ref);
            if (criterion != null) {
  //把该排序方法从下拉列表中删除
                sortedByList.removeItem(criterion.getSortFieldString());
                this.sortingFields.remove(criterion);
            }
            return;
        }
    }
}
 
与(五)相比不同的地方是,这个gui的table model以及排序的方法,都是通过查询service对象获得。
 
manifest文件如下:
Manifest-Version: 1.0
Bundle-SymbolicName: com.bajie.test.family.gui
Bundle-Name: family gui
Bundle-Version: 1.0
Bundle-Vendor: LiMing
Bundle-Activator: com.bajie.test.family.gui.FamilyInfoGui
Import-Package: org.osgi.framework;version=1.3,com.bajie.test.family.model
 
然后我们生成bundle的jar文件。分别为familymodel.jar和familygui.jar,之后我们用“in”命令把两个bundle装入framework。
接着我们先启动model bundle,然后再启动gui bundle,我们会看到JTable中有3列,而排序方法列表中也有3个选项,完全和程序的逻辑符合。
 
接下来,我们假设客户需要添加显示每个家庭的人均年收入并按其排列纪录。要满足这个需求,我们可以参考在(五)中做法,就是在model bundle里面再添加一个同时实现了FamilyInfoColumn和SortingFamilyInfoCriteria的类,并在bundle的启动中作为服务注册到framework中?不过这样就得更新model bundle然后调用rfr命令来刷新。为什么不再装一个补丁bundle,在这个bundle中包含了同时实现FamilyInfoColumn和SortingFamilyInfoCriteria的类,并在这个新bunle启动时注册产生该类的新对象作为服务注册到framework中,这样gui和model bundle都能侦听到该新服务的到来(他们都实现了服务侦听接口ServiceListener),gui上马上就能有所体现。

这个新bundle的代码如下:
package com.bajie.test.family.model.impladd;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import com.bajie.test.family.model.FamilyInfoColumn;
import com.bajie.test.family.model.FamilyInfoEntry;
import com.bajie.test.family.model.SortingFamilyInfoCriteria;
public class FamilyIncomePerPerson implements BundleActivator {
    public void start(BundleContext context) throws Exception {
 //注册一个新的服务,服务的类型既为FamilyInfoColumn,也是SortingFamilyInfoCriteria
        String[] clazzes = new String[] {FamilyInfoColumn.class.getName(),SortingFamilyInfoCriteria.class.getName()};
        context.registerService(clazzes,new FamilyIncomePerPersonColumn(),null);
       
    }
    public void stop(BundleContext context) throws Exception {
    }
    //这个类实现了“Income Per Person”这个列以及按该列排序的方法。
    class FamilyIncomePerPersonColumn implements FamilyInfoColumn,SortingFamilyInfoCriteria {
        private static final String COLUMNNAME = "Income Per Person";
       
        public Object getColumnValue(FamilyInfoEntry entry) {
            return new Integer(entry.getIncomePerYear()/entry.getPopulation());
        }
       
       
        public String getColumnName() {
            return FamilyIncomePerPersonColumn.COLUMNNAME;
        }
       
        public String getSortFieldString() {
            return FamilyIncomePerPersonColumn.COLUMNNAME;
        }
        public int compare(Object obj1, Object obj2) {
            if (obj1 == obj2) {
                return 0;
            }
            FamilyInfoEntry en1 = (FamilyInfoEntry)obj1;
            FamilyInfoEntry en2 = (FamilyInfoEntry)obj2;
           
            return en1.getIncomePerYear()/en1.getPopulation() - en2.getIncomePerYear()/en2.getPopulation();
        }
       
    }
}
 
manifest文件如下:
Manifest-Version: 1.0
Bundle-SymbolicName: com.bajie.test.family.modeladd
Bundle-Name: family model add
Bundle-Version: 1.0
Bundle-Vendor: LiMing
Bundle-Activator: com.bajie.test.family.model.impladd.FamilyIncomePerPerson
Import-Package: org.osgi.framework;version=1.3,com.bajie.test.family.model
 
打包安装到framework后,启动该bundle,我们就会在gui上看到新的列已经被添加,而且排序列表中增加了一个新的排序选项。
这个结果,完全符合需求的意图。
如果我们用stp命令停止这个bundle,我们在gui上就会发现,新列消失,而且排序列表中对应选项也没有了。这就是service带来的动态效果。不过,如果我们的model发生了一些实质的变化,比如FamilyInfoEntry需要添加一个“地址”列,那么model bundle就要更新,进而gui bundle以及使用到这个类型的bundle都需要通过rfr命令刷新。
 
好了,对扶贫助手的分析就此打住,我们总结一下,通过程序可以看到注册服务一点都不复杂。最简单的情况我们只需要提供一个java类型名称,以及实现这个类型的一个java对象就可以了,
不需要提供复杂的类型描述,比如xml描述文件。而使用服务的bundle通过类型名称就轻而易举的查找到相关的服务对象。
 
到此,osig介绍系列就要结束了,只希望这个系列能够把你引入到osgi的门口,其后面的精彩世界就看你的兴趣了。
就我个人的关注和理解,今年是osgi很重要的一年。JSR249今年应该投票,如果osgi入选,那么osgi将成为高端手机中java体系结构的重要组成部分。
在汽车领域,siemensVDO已经推出了基于osgi的解决方案,听说已经配备在BMW serials 5里面了。应该还会有更多的应用......
 
如果你是osgi的粉丝,欢迎你来信jerrylee.li@gmail.com拍砖交流。

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

评论

# re: OSGi介绍(六)OSGi的service 2006-02-15 09:50 BlueDavy

^_^,现在注册服务以及使用服务的部分在osgi中还是不那么方便的,注入会让它变得更加方便,其实好像ds是解决了部分,但还没完全做到注入..  回复  更多评论   

# re: OSGi介绍(六)OSGi的service 2006-02-15 12:36 勤劳的蜜蜂

ds是什么?有相关的比较文章吗?我看看,谢谢。  回复  更多评论   

# re: OSGi介绍(六)OSGi的service 2006-02-15 13:15 BlueDavy

OSGI中的Declartive Service  回复  更多评论   

# re: OSGi介绍(六)OSGi的service 2006-02-15 13:21 勤劳的蜜蜂

哦,它呀。正在研究中,听说是extension point的替代品  回复  更多评论   

# re: OSGi介绍(六)OSGi的service[未登录] 2007-04-16 17:48 andy

谢啦 楼主好幸苦 不过感觉这个例子太长 看起来有点费劲 最好能找个简单一点的 大家看着方便 不知道您对ds有什么看法 我的邮箱是qmiao128@163.com 可以交流一下吗  回复  更多评论   

# re: OSGi介绍(六)OSGi的service 2007-06-22 11:59 hata

太长了 看晕了.没理解service的机制.
继续看看...晕晕了.  回复  更多评论   

# re: OSGi介绍(六)OSGi的service 2007-07-05 15:36 guyong

context.addServiceListener(this,"(" + Constants.OBJECTCLASS + "=" + FamilyInfoColumn.class.getName() + ")");
请问一下这个给bundle添加监听的方法中的第二个参数是什么意思?格式是固定的吗?谢谢!
我的邮箱:guyong1018@yahoo.com.cn  回复  更多评论   

# re: OSGi介绍(六)OSGi的service 2007-07-06 13:49 ferrari4000

第二个参数的意思是,这个listener只侦听服务对象满足 instanceof(FamilyInfoColumn.class) == true的ServiceEvent.
当有服务事件发生时,Framework会给每个注册的listener发送消息,但是,如果服务注册的时候,有过滤条件,它会先检查这个事件是否满足这个过滤条件,如果不满足,那这个事件就不会发送到这个listener.

osgi spec里面对这个格式进行了定义.请看第37页@r4.core.pdf version4.01 July 6, 2006版
  回复  更多评论   

# re: OSGi介绍(六)OSGi的service 2007-09-25 09:55 hello

太长了,很难看得懂,感觉!
看了几次都看不下去!
  回复  更多评论   

# re: OSGi介绍(六)OSGi的service 2008-07-25 10:16 mhoudg

汗……
楼主写了这么多,都没喊辛苦
反倒是来学习的人们一个劲喊长喊累……
这世道……  回复  更多评论   

# re: OSGi介绍(六)OSGi的service 2008-10-31 00:03 bacuo

不错不错,感谢楼主的辛勤劳动  回复  更多评论   

# re: OSGi介绍(六)OSGi的service[未登录] 2008-11-19 11:00 rex

非常感谢作者的工作!  回复  更多评论   

# re: OSGi介绍(六)OSGi的service[未登录] 2008-12-12 15:07 mark

看后受益匪浅,非常感谢楼主  回复  更多评论   


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


网站导航: