By [email protected], 3/6/07 |
|
Netbeans在近年的发展,可说是长足的进步。它不仅是功能强大的集成开发环境,更可以看作是一个开发框架和平台,基于这个平台,通过模块开发,扩展这个平台的功能,或者根据自己的需求,定 制个性化的IDE环境。
Netbeans IDE由一个核心运行时环境(core runtime)和一组模块组成。这个core runtime为大多数桌面应用提供公共组件和服务,而“模块”,则是运行在这个core runtime之上的java class,譬如,对于Java语言的支持,就是一个"plugin module",所有Netbeans IDE的跟开发相关的功能都是以“模块”的方式提供的。
开发者可以根据Netbeans平台所开放的编程接口,开发自己的"plugin module"来实现特定的功能。一般来说,有这么两个目的:
从程序的角度看,一个module就是一个java的archive(.jar)文件,它包含多个java类,以及一个manifest文件,用来标志该jar文件是一个Plugin Module。当 平台的main class运行时,找到所有可用的module,建立一个内存登记表,并且运行这些module指定的在平台启动时的代码,module的其它代码则根据需要装载。
从发布的角度看,一个module是一个.nbm文件,使用者可以从利用Netbeans IDE中的“update center”功能,从网站下载.nbm文件并安装,或 者选择本地的.nbm文件进行安装。
Netbeans从5.0版本开始,提供了向导(Wizard)和模板template,来帮助Netbeans module的开发。一般的开发过程是在基于Wizard生成的Template之上,添 加功能。下面以一个简单的Plugin Module开发过程(参考:http://platform.netbeans.org/tutorials/nbm-google.html)为例,来 介绍这个过程:
1. 创建Plugin Module Project
在创建project的界面上,如图所示,有三类:
这里选择Module Project就可。
Library Wrapper Module有什么用途?它帮助我们在Netbeans Plugin开发中和第三方jar文件建立依赖关系。举例:如果要申明本模块依赖test.jar,
当然,如果模块依赖Netbeans本身提供的module,比如Data System API,只需最后一步即可,不用Library Wrapper Module。
这和我们在普通的java程序开发过程中,申明和.jar之间的依赖关系的方法有所不同,平时,我们只需将.jar文件放到classpath上即可。Netbeans Module申明依赖关系的方式的好处在于保持很好的模块化,将模块之间的依赖关系最小化,清晰化,并且利用netbeans中的public package的功能,隐藏API的实现细节,利 于模块将来的升级和维护。
在接下来的界面中,给模块命名(MyFirstModule),选择模块所在的目录(d:/test/nbmodule),生成的java的包名(org.myorg.myfirstmodule)。点击“ finish”就完成了模板的建立。
2. 让我们看看模板中都包含些什么:
源文件和单元测试的包。除了包含和大多数其它普通项目一样的源文件,它还包含一个重要文件:
缺省在org.myorg.myfirstmodule的源文件包下,有一个“layer.xml”文件。它以xml的格式定义了系统的可扩展点,如新增加的 Menu,T oolbars,action属性等以及用户自定义的配置信息,这些信息将合并到整个netbeans系统的配置注册表中(以后会谈到,这个系统的配置注册表称为System FileSystem)。浏 览layer.xml文件的内容,会发现它包含了“FileSystem”这样的tag。
Important Files:这个目录下包含了Plugin Module项目中重要的配置文件,其中比较重要的有:
XML Layer:它是上述的layer.xml文件的图形表达方式,可以通过图形界面对layer.xml进行浏览和修改
Module Manifest:位于项目根目录下的manifest.mf 文件指明该项目是一个plugin module,并 包含layer.xml和bundle文件的位置
Project Metadata:包含项目的元数据,如项目类型,该模块和别的模块依赖关系等等。在Library Wrapper Module Project介绍中 提到的依赖关系即在这里申明。
3. 在生成的项目上,就可以利用IDE提供的wizard来创建各种文件模板:
如图所示,Netbeans为plugin的开发提供了多种模板以供选择。除了常见的Java Package,Java Class和File/Folder等,有些模板是Plugin Module开发专用的,它们提供了扩展系统的一些方式,运用模板能够迅速的搭起框架,或者获得示例的代码:如,Project Template可以以提供的项目结构和源码为基准,建立新的模板,加 入到主菜单的“File”-> “New Project”的可选项目模板中;Wizard可以帮助你建立一系列向导窗口;Window Component可以用来创建以 TopComponent为子类的窗口类,并且可将对应的窗口对象嵌入到IDE的指定位置;等等。值得注意的是,并不是所有的系统扩展和新功能的添加都能够,或者必须通过模板的方式来实现,如,希 望在Editor中添加代码自动完成的功能,则没有可用的模板,开发者应熟悉在layer.xml中可以添加的系统的扩展点,以及对应的code completion API。
这里,我们以生成一个新的Action为例。目的是要在系统的工具条上加上一个google搜索栏。当在搜索栏中输入文字,回车后,将调用GoogleAction。因此,在模板列表中选择“ Action...”,出现
我们知道,凡菜单项或者工具条选项,都有两个状态,激活或者禁止。这里界面上有两个选项正是为这两种状态设置。“Always Enabled”,如名称所示,这个Action总是处于可调用的状态,因 而界面上的选项将总是处于激活的状态。而“Conditionally Enabled (use CookieAction)”则是根据当前所选择的界面对象的状态和条件,来决定是否激活Action。关 于界面上看到的DataObject,Node和Cookie等,我们将在后续的内容中介绍。这里简单起见,选择“Always Enabled”,下一步的界面“GUI Registration”:
容易推测出,这一步为Action在界面上注册一个调用入口,并且指定它的位置。它可以是一个菜单项,也可以工具条上的按钮,或者对应某个快捷键等。指定Toolbar和Position之后,在 接下来的步骤中指定类名GoogleAction等信息后,就完成了Action模板的建立。
让我们看看Netbeans自动为我们做了什么:
4. 基于自动生成的模板,运用Netbeans API编写代码,实现所需功能。
本例将创建JPanel 表单,并和GoogleAction关联。在项目名字上右键弹出的菜单中,选择“new”-> “JPanel Form”,在生成的Panel界面上,建立表单,编 写代码实现Google Search。最后重载GoogleAction类的getToolbarPresenter()方法,将JPanel实例作为返回值,即可。具体的步骤请参见 http://platform.netbeans.org/tutorials/nbm-google.html
5. 测试和安装
这两步也非常简单。选择右键菜单中的“Install/Reload in Target Platform”,即可以测试。选择“Create NBM”,可以创建可发布的nbm文件, 用户则可以用菜单中的“Tools”->“Update Center”来安装。如果想卸载,则可用“Tools”-> “Module Manager”功能。
这是非常简单的例子,但是在这个例中,我们介绍了plugin module的几种类型,创建和发布plugin module的一般步骤,module的基本组成,如何解决 netbeans module dependency,怎么扩展系统的菜单,以及action API的基本用法。但是,在这个示例的介绍中,也留下了很多疑问,比如什么是 System FileSystem,D ataObject,Node等等。在下文中,将比较深入的介绍基于netbeans平台,进行Plugin Module开发所涉及到的基本概念,以及其中API的使用。
我们知道,构建一个框架或者平台软件,必须考虑平台的通用性以及模块和平台之间的接口。当我们学习怎么基于netbeans的平台,做模块开发的时候,不仅 是单纯的开发过程,也 是一个了解netbeans的设计思想的过程。
在netbeans 3.x的版本的IDE中,开发者经常要mount“filesystem”到一个虚拟的名字空间中,每个文件系统都有一个根(“root”),所 有在这个根下的文件都存于这个名字空间中。在netbeans 4.0以后,对于普通IDE使用者而言,在界面上,文件系统的概念已经不存在了,但是类似的概念仍然存在于IDE的基础架构中,开发plugin module仍需涉及。
Netbeans中,文件系统(FileSystem)扮演着双重角色:它既可以代表磁盘上的物理文件系统,也可以代表IDE和module的配置数据构成的虚拟的文件系统。基于IDE 的配置数据的文件系统称为“System FileSystem”。在当前的FileSystem API中,可以从一个称为Repository的singleton对象里,用 Repository.getDefault().getDefaultFileSystem()方法获得“System FileSystem”,开发者利用System FileSystem,存 取系统的配置数据,以及特定配置目录下的磁盘文件。其它常见的文件系统有JarFileSystem和LocalFileSystem等。LocalFileSystem代表本地磁盘的文件系统。无论是物理的,还 是虚拟的文件系统,它们都共享FileSystem的抽象特性,以及基于其上的FileObject,DataObject,Node等概念。
从文件系统中,获得的是FileObject对象,它提供关于文件对象的基础信息(名字,父对象,是否存在等),和在对象上的操作(移动,删除等)。FileObject和 java.io.File的主要不同之一在于FileObject提供了change listener的机制。另外,FileObject并不一定是代表磁盘上的物理文件,这种情况在接下来介绍中,可 以看到实例。
物理的文件系统(如LocalFileSystem)是比较好理解的,可以参考相关API。这里,我们更需要关注关于System FileSystem的几个基本问题:
1. Netbeans plugin的配置数据是如何表示的?
2. 配置数据与System FileSystem的关系?
3. System FileSystem的根(“root”)在哪里?
4. System FileSystem的扩展点
5. 如何取得System FileSystem根下的物理文件?
6. 如何取得System FileSystem里的虚拟文件对象?
上文提到,每个module都是通过一个“layer.xml”文件来保存配置数据,文件中包含一些虚拟的“文件”和“目录”。这些layer.xml合并在一起就构成了System FileSystem。module就是通过这种方式把配置数据加载到系统中。在module的jar文件中的manifest中,指明了这个layer.xml所处的位置,例如:
OpenIDE-Module-Layer: org/myorg/myfirstmodule/layer.xml
根据这个信息,可以在classes目录中找到这个文件。
一个简单的layer.xml文件就像:
其中有一些“folder”是系统预先定义好的,比如,例中的“Actions”和“Toolbars”,当module往IDE的toolbar上添加新的选项的时候,在 “Toolbars”以 及它的子目录下,必然出现对应与这个新选项的“file”条目和相关的属性。可以看到,这些预定义的folder正是这个系统的扩展点。
还有一些folder是用户自定义的,比如例中的“myFolder”,注册于System FileSystem中,可以动态保存用户自己的配置或者资源信息。这些信息可以以虚拟文件的形式,存 于layer.xml中:
FileObject vFile = Repository.getDefault().getDefaultFileSystem().findResource("myFolder/vFile");
System.out.println("the attr is:" + vFile.getAttribute("attr1"));
程序将打印出"the attr is attrValue"。这是一个在System FileSystem中代表虚拟文件的FileObject的例子。当然,如果需要一个物理文件作为实体,也可以用“ url”属性建立虚拟文件和物理文件之间的映射。如例中的
。 用下面的代码,程 序将获得相对于layer.xml路径的"resource/realfile"物理文件,并且构造出File Object: FileObject res = Repository.getDefault().getDefaultFileSystem().findResource("myFolder/myFile.txt");
另外,在运行过程中,还可以在System FileSystem的根下,往myFolder目录下添加新的物理目录和文件:
FileObject myFolder = Repository.getDefault().getDefaultFileSystem().getRoot().getFileObject("myFolder"); myFolder.createData("newFile");
程序将获得System FileSystem根下myFolder目录对应的FileObject,并调用createData方法,在这个目录下创建一个“newFile"文件。在 module的开发调试环境中这个“根“位于
更cool的是,在System FileSystem中,可以使FileObject成为java对象的工厂。在上例中以.instance为后缀的文件,就是这样的例子。我们在介绍Data System API的时候会详细介绍。
可见,在netbeans中,文件系统以及文件对象扮演了多种角色,但是它们基于同一抽象的特性,使下面介绍的DataObject,Node等能广泛适用。
DataObject是FileObject的wrapper。FileObject代表的是一个类似于文件的实体,而DataObject代表的则是文件内容的模型。文件类型是通过后缀名来标志的,一 般来说不同的文件类型对应于不同的DataObject的实现。DataLoader是创建DataObject的工厂类,如果程序需要支持新的文件类型,则应实现相应的DataLoader 和DataObject,并在module的manifest中注册,如:
Name: org/netbeans/modules/povray/PovDataLoader.class
OpenIDE-Module-Class: Loader
如果不需要支持新的文件类型,则不用和DataLoader打交道。只须用下面的方法获得某个fileobject的DataObject: DataObject.find(someFileObject);
和DataObject交互的方式是通过getCookie()和getLookup()方法。这两个方法应该是可以互换的。getCookie()在将来的netbeans版本中,将 被getLookup所替代。注意,这里的Cookie和http协议中的Cookie没有关系。它代表的是这个DataObject能够提供的操作或者服务。用法如下:
OpenCookie open = someDataObject.getCookie (OpenCookie.class);
if (open != null) {
open.open();
}
如果在someDataObject上如果支持open操作,则从这个DataObject中获得实现了OpenCookie接口的对象,然后调用这个对象上的open操作。这样,DataObject 中并不实现具体的操作,接口实现代码封装在cookie中,通过getCookie和getLookup(关于Lookup,在后面介绍)的编程接口,提供给调用者。
前面提到,System FileSystem中,以.instance为后缀的文件构造的FileObject,能成为java对象的工厂。让我们看看是怎么实现的:在例中的lay.xml中,
//fo中包含了要构造的对象的类名。
FileObject fo = Repository.getDefault().getDefaultFileSystem().findResource("Actions/Edit/org-myorg-myfirstmodule-GoogleAction.instance")
DataObject dobject = DataObject.find(fo); //根据后缀名,获得DataObject对象。
InstanceCookie ic = dobject.getCookie(InstanceCookie.class) //从DataObject中,获 得InstanceCookie的实现
if (ic != null) { //如果该DataObject支持创建新的实例的操作
GoogleAction myAction = (GoogleAction)ic.createInstance(); //利用Cookie接口,创建Java对象
}
和以.instance为后缀名的情况类似,如果文件对象的后缀名是.ser,即对象的串行化的结果文件,也可以通过获得InstanceCookie的方法来创建被串行化的对象。
前面提到,FileObject是文件的抽象,DataObject是文件内容的模型,而Node是表现层的对象。它们有actions(操作),properties(属性),本 地化的显示名称以及图标等等,封装了对象在人机接口方面的特性。需要注意的是,Netbeans 的Node不是GUI对象,和JDK中的TreeNode没有继承关系。但类似的是,它一般来说,也 是一个树型结构,每一个Node有Children对象,提供子节点的列表。
//Find a file on disk
FileObject f = Repository.getDefault().getDefaultFilesystem().getRoot().getFileObject("lsome/folder/someFile.txt");
//or if something passes you a File...
FileObject f = FileUtil.toFileObject (new File("some/folder/someFile.txt"));
//Turn a FileObject into a File (may fail for virtual filesystems)
File f = FileUtil.toFile (someFileObject)
//Get the DataObject for a FileObject
DataObject obj = DataObject.find (someFileObject)
//Get the FileObject a DataObject represents
FileObject file = someDataObject.getPrimaryFile();
//Get the Node that represents a FileObject
Node n = someDataObject.getNodeDelegate();
//Get the DataObject a Node represents (if any)
DataObject obj = (DataObject) someNode.getLookup().lookup(DataObject.class);
public Action[] getActions(boolean popup) {
DataFolder df = (DataFolder)getLookup().lookup(DataFolder.class);
return new Action[] {
new AddRssAction(df),
SystemAction.get(DeleteAction.class),
SystemAction.get(SaveAction.class)
};
}
当该Node被选中,在点击右键弹出的菜单中,会有三个选项,一个是“add”操作,操作的实现在AddRssAction中,该操作是在激活状态,因为AddRssAction就是这 个action的实现;第二个是系统Action中的“delete”操作。操作如“delete”,“copy”,“paste”,“cut”,它们的实现可以由ExploreUtil的工厂方法来 提供,它 们的激活与否是由ExploreManager以及当前的剪贴板等状态来控制的。第三个选项是系统Action中的“save”操作,它的激活与否在于该Node的Lookup或者 cookie中有没有SaveCookie的实现。简而言之,getActions()返回了右键菜单中的选项,但是这些选项是否激活,取决于运行系统是否能以各种方式获得对应action的 实现。< /p>
在DataObject和Node的介绍中,多次提到Lookup。Lookup( http://www.netbeans.org/download/dev/javadoc/org-openide-util/org/openide/util/Lookup.html)类,是 被广泛应用于netbeans API中一个很重要的概念,我们也可以把它看成是Netbeans版本的IoC的实现,它也是各个模块之间实现decouple的关键所在。可以先简单的把它理解为,它 是一个Map,其中keys是class对象,对应每个key的value,是这个class的实例。一般来说,这个class对象是一个interface,定义了某服务的接口,而class实例,是 这个服务的实现。
在Netbeans中,有两种Lookup的用法。一种是用于从全局的服务登记表中获得某个服务的实现,Lookup.getDefault()调用返回的Lookup对象,即这个全局的服务登记表;另 一种,局部的用法,用来查询某个对象是否支持某个服务,并且获得它所支持的服务的实现。很多类型的对象都有自己的Lookup,比如TopComponent,DataObject,Node等等,可 以用类似于obj.getLookup()调用来获得obj的Lookup,这个Lookup对象里存的是当前上下文信息。对于调用者来说,只需获得它所关心的对象的Lookup,从中检索并调用相应的服务。& amp; amp; amp; lt; /p>
这里我们主要探讨一下Lookup的局部用法。用户在IDE环境中的常用操作之一是,移动焦点,并且“选择”某个对象,在这个对象上执行相应操作。当前焦点所在的对象,也称当前选中的对象,活动的对象,或 者处于激活状态的对象。Netbeans IDE菜单中的常见功能,比如“打开”,是在当前所选中的对象上执行“打开”操作。显然对于不同的对象,“打开”操作的实现是不一样的。当选择不同的对象的时候,也 可见属性窗口中的属性值随着对象的不同而变化。那么在同样一个框架下,如何判断某个特定对象是否支持“打开”这样的公用操作,并且执行操作?当焦点对象变化的时候,框架如何得到通知,从而作相应的变化?在 Netbeans中,Lookup就是解决类似问题的答案。
例如下面这个例子就是从Node对象中获得它的Lookup:
Node[] n = TopComponent.getRegistry().getActivatedNodes();
if (n.length == 1) {
OpenCookie oc = (OpenCookie) n[0].getLookup().lookup(OpenCookie.class);
if (oc != null) {
oc.open();
}
}
这段代码,就可以用于实现上文描述的关于“打开”操作的场景,Lookup的应用,使得“打开”这个操作不需关心当前活动的节点的内容,也不用关心该节点将怎么执行 “打开”操作,只 要这个节点的Lookup中能够检索到OpenCookie接口的实现,调用其open函数即可。这样就实现了模块之间的解耦。关于Lookup的更细节的用法说明,请参见:
Lookup的基本概念:
http://openide.netbeans.org/lookup/
用Lookup SPI构造自己的Lookup:
http://www.netbeans.org/download/dev/javadoc/org-openide-util/org/openide/util/lookup/doc-files/lookup-spi.html
Lookup API的使用:
http://www.netbeans.org/download/dev/javadoc/org-openide-util/org/openide/util/lookup/doc-files/lookup-api.html
在Window System API中,最常用到的是TopComponent类,它是可嵌入式的窗口组件的父类,负责跟Netbeans的窗口系统交互。在 前面介绍Netbeans提供的代码模板类型中,就提到TopComponent。选择“Window Component”模板自动生成的java类,就是TopComponent的子类。Top component可以是一个单独的窗口,也可以嵌入在tab页中。Netbeans的整个界面环境被划分为不同的部分,每个部分都有名称。比如:“explorer”,一般指位于界面左上角位置的窗口部分,“ Project”,“Files”,“Runtime”这些都是位于“explorer”位置的窗口组件。"Editor",指的是位于界面中间的部分,比如各种编辑器都是位于这个部分。生成新的窗口组件时,会 要求指定窗口所处的位置。
每一个TopComponent实例都有一个Lookup的对象来标志其上下文,有一组Explorer view对象作为其界面元素,和一个或者多个激活的节点(Nodes)。程序中,一 般用associateLookup方法来设置TopComponent的Lookup对象,而当前激活的nodes则一般从下文将要介绍的ExplorerManager中得到。
Nodes提供了一组层次化的对象,而Explorer API则提供了界面对象(Explorer View)来显示这些Nodes。但是Nodes和界面对象view之间,并没有直接的调用关系,它 们之间的交互是通过ExplorerManager来实现的。用MVC的观点看,ExplorerManager就是Controller,它提供一个或者多个view要显示的node内容,管 理了views之间的共享的状态,如当前选中的节点和它的Lookup。当用户在某个view上的操作导致共享状态的改变时,该view对象会调用ExplorerManager的方法更新共享状态,E xplorerManager再将共享状态的改变通知到其它的view。结合这些概念,来看一段代码:
class FeedTopComponent extends TopComponent implements ExplorerManager.Provider {
private final ExplorerManager manager = new ExplorerManager();
private final BeanTreeView view = new BeanTreeView();
private FeedTopComponent() {
add(view, BorderLayout.CENTER);
view.setRootVisible(true);
try {
manager.setRootContext(new RssNode.RootRssNode());
} catch (DataObjectNotFoundException ex) {
ErrorManager.getDefault().notify(ex);
}
ActionMap map = getActionMap();
map.put("delete", ExplorerUtils.actionDelete(manager, true));
associateLookup(ExplorerUtils.createLookup(manager, map));
}
public ExplorerManager getExplorerManager() {
return manager;
}
...........
}
示例中,FeedTopComponent即上文提到的TopComponent的子类,BeanTreeView作为一个Explorer View对象加入到了窗口对象中。B eanTreeView需要显示的节点,以及共享状态是由ExplorerManager来控制的。通过调用ExploreManager的setRootContext方法,设置了这组节点的根。
因为ExplorerManager管理了nodes的状态,所以前面提到能从TopComponent窗口实例中获得一组当前激活的节点,实际上,是从其内置的ExplorerManager 中获得。T opComponent的Lookup对象用ExplorerUtils.createLookup(ExplorerManager,ActionMap)方法创建,使 得TopComponent的Lookup实际上,代理给了由ExplorerManager管理下的当前活动节点的Lookup和ActionMap中支持的action操作。
ActionMap对象维护了action的名字和对应的action实现的映射。示例中,ActionMap添加了“delete”操作,通 过把ActionMap和ExplorerManager一起加入到TopComponent的lookup中,使“delete”操作能够在ExplorerManager管理下的nodes上执行。也 就是说当某个node选中的时候,系统的“delete”菜单会被激活。
Netbeans中还有很多别的API,可以参见Netbeans的网站上javadoc和相关资料。当然关于API的了解,往往也是在实际运用中得到增强的。
本文首先介绍了创建Netbeans Plugin Module的一般步骤,然后就开发过程中要涉及到的基本概念做了探讨,目的在于了解Netbeans Plugin Module开发中的基本思想和要素,为更深入的学习和理解打下一定的基础。
http://platform.netbeans.org/tutorials/index.html
http://www.netbeans.org/download/dev/javadoc/