Java 的插件机制

现在基于插件机制的软件越来越流行,这是一个好事情,给软件本身提供了很好的可扩展性,可以让第三方的开发人员基于提供的接口开发自己的插件。不管是哪个平台上,Windows 也好,Linux 也好,此类的软件是数不胜数,媒体播放器([foobar2000])、即时通讯软件([Gaim])、浏览器([Mozilla Firefox])、甚至于 Linux Kernel。在如果没有了可扩展性,恐怕很多人都不会去用这个软件,如果想要稍微改变一下软件的行为,或者增加更多的支持,就要去修改主程序的源代码,感觉上并不那么好。

不管叫插件(plugin)或者叫模块(module),还有扩展(extension),意思都类似。不过似乎稍微还是有点差别。扩展和模块比较类似。模块可能是更为独立,模块之间的相似性比较少,很可能两者之间根本没有关系,比如 Firefox 的扩展,可以做各种各样不同的事情。而插件呢,更可能是同一种类型的扩展,范围可能更窄一些,比如媒体播放器的解码器插件,针对不同的音频视频格式进行解码。

实现方式上基本上都相同,Windows 上使用动态链接库(dll),*NIX 上使用共享对象(so)。C 实现一般使用 dlopendlsymdlclose 等函数操作从动态库中获取和运行具体的函数。Java 本身就具有动态装载的功能,并使用反射(Reflection)机制运行代码,已经在 Java 世界包括 JDK 本身广泛应用。

一般模块或者插件机制的实现,包括几个步骤:

  1. 发现插件。C 中一般是把所有的模块或者插件放在某个目录下,主程序起来以后,列举目录中的文件,再下一步检查是否是有效的插件。
  2. 装载插件。C 中使用 dlopen() 打开一个具体的共享库,然后使用 dlsym() 检查一些特定的函数名,是不是程序所需要的,然后再做进一步的验证等。
  3. 运行插件中的代码。C 中通常都是函数指针,使用 dlsym() 的结果就是需要的函数指针,就可以直接运行。

而 Java 由于本身的一些特点,某种程度上比 C 要简单一些。根据实现方法的不同,会稍有不同。

  1. 使用系统的 ClassLoader。举个 XML Parser 的例子,我们知道,在 JAXB(Java Architecture for XML Binding)中,使用 DocumentBuilderFactory 来创建 DocumentBuilder,那可能有很多种不同的 XML Parser 的实现,需要不同的 DocumentBuilderFactory 的实现。当然你可以直接指定到一个具体的实现,比如 ab.cd.DocumentBuilderFactoryImpl,然后将包含这个类的 jar 包放在 CLASSPATH 中,直接 new 就可以了。但这不是我们所希望的,希望能够不改代码,可以直接换用别的解析器。

    Java 中提供了一种方法来实现这种功能。[JAR File Specification] 中指定了一个 META-INF/services/ 目录。可以在里面放置一些文件,文件名字为接口的名字,这里就是 javax.xml.parsers.DocumentBuilderFactory,文件内容只有一行,就是包含在这个 jar 包中实现这个接口的子类,比如 [Jakarta Xerces] 的实现,在 xerces.jar 里面,内容为 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl

    那么如何找到这个文件呢?我们先看看 JAXB 里面是怎么做的:

    • javax.xml.parsrs.DocumentBuilderFactory.newInstance()
    • javax.xml.parsrs.FactoryFinder.find(String, String)
    • javax.xml.parsrs.FactoryFinder.findJarServiceProvider(String)
    • xxx.getResourceAsStream(ClassLoader, String)
  2. 那么关键就是 xxx.getResourceAsStream,简单来说,就是在当前的 CLASSPATH 中,按顺序找相应的文件,这里就是 META-INF/services/javax.xml.parsers.DocumentBuilderFactory,然后将里面的内容(类名)读出来,使用反射机制来创建具体的实例。然后就可以使用了。这种机制称为“Service Provider API”。

    但这样就有一个问题,这样必须将 jar 包放在 CLASSPATH 中,并且一个接口只能找到一个实现,在某些情况下是不够用的,比如媒体播放器,要支持多种解码器,现在只能支持一种,支持了这种,就支持不了那种,显然是不可用的。并且还可能需要动态支持,把相应的包仍进去就可以用,这仍然是不支持的。这就得要使用下面一种方法。

  3. 自定义 ClassLoader。为一个应用程序编写的插件,一般情况下是不能直接用于别的程序的,也就是插件的特定性。那么就可以把这些插件放在一个目录里面,在应用程序启动的时候,把这个目录里面所有的包,使用我们自定义的 ClassLoader 读进来。包的格式可以仍然使用上一种方法,在 META-INF/services/ 里面以接口类名作为文件名,内容为实现类名。把所有这些都读入以后,我们就知道当前有多少个插件实现了某种接口,就可以直接使用。

    那有什么缺点呢?需要自己写个 ClassLoader,自己去找所有的文件,可能会稍微麻烦一点。而且这部分代码针对不同的程序结构是类似的,但具体的内容不同,可能存在重复代码的问题。

当然,这是最简单的情况,还有其他很多的问题需要考虑,比如安全问题、版本问题等等。

所以自然就有做出一个通用的架构这样一种需求。[Java Plug-in Framework] 就提供了这样一种架构,它使用 XML 作为插件描述文件,并提供了一个演示程序。简单扫了一眼以后,发现有个问题,本来我以为它就是一个库,应用程序只要调用它就可以了。但实际上不是这样,它把应用程序的入口也作为一个插件,完全通过它的框架来启动运行。感觉有点奇怪。不过看其特性,还是很丰富的。当然我也没有再仔细地去看。

今天在看 blog 的时候,发现另外一个东西,[JSR-277 Java Module System] [发布了] early draft,然后一些人给了评论,[OSGi Peter Kriens][robilad][Patrik Beno]。上面提到 JSR-277 将会在 JDK 7 中出现。

但另外一个 [JSR-291 Dynamic Component Support],看着就是上面的 [OSGi] 系统的原型,介绍中也提到了。不知道这两者是一个什么关系,不管从上面 Peter Kriens 的评论看来,他对这个很不爽,看来是竞争关系。呵呵。

这两份标准都是刚出的 early draft,我指示扫了一眼,没仔细看,有空再详细看一下。

More JSR-277 Early Draft Q & A

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <blockquote>
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>. Beside the tag style "<foo>" it is also possible to use "[foo]".
  • You can use BBCode tags in the text. URLs will automatically be converted to links.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
9 + 2 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.