模块化Java:动态模块化
副标题#e#
在前一篇文章《模块化Java:静态模块化》中,我们接头了如何构建Java模 块并将其作为一个单独的JAR举办陈设。文中的例子给出了一个client和一个 server bundle(两者在同一个VM中),client通过工场要领找到server。在该 例子中,工场实例化了一个已知类,虽然也可以利用反射来获取一个处事实现; Spring就是大量运用这种技能把spring工具绑定在一起的。
在我们接头动态处事之前,有须要回首一下类路径,因为尺度Java代码和模 块化Java代码的区别之一就是依赖在运行时是如何绑定的。在此之后,我们还将 简朴接头一下类的垃圾接纳;假如你对此已很是熟悉,则可以跳过这部门内容。
Bundle ClassPath
对付一个普通Java措施,只有一个classpath——启动应用措施所利用的谁人 。该路径凡是是在呼吁行顶用-classpath选项指定的,可能通 过CLASSPATH 环 境变量来设定。Java类装载器在运行时理会类的时候会扫描此路径,无论这一过 程是静态地(已编译进代码)照旧动态地(利用反射及 class.forName())。然 而,在运行时也可以利用多个类加载器;像Jetty和Tomcat这样的Web应用引擎都 是利用多个类加载器,以便支持应用热陈设。
在OSGi中,每个bundle都有其本身的类加载器。需要被其他bundle会见的类 则被委派(delegated)给这些其他bundle的类装载器。因此,尽量在传统应用 中,来自logging类库、client和server JAR中的类都是由同一个类加载器加载 的,但在OSGi模块系统中,他们都是由本身的类加载器加载的。
功效是,一个VM中有大概有多个类加载器,个中大概存在名字沟通的差异 Class的工具。也就是说,在同一个VM中,一个叫做 com.infoq.example.App的 类,其差异版本可以由com.infoq.example bundle的第1版和第2版同时输出。 Client bundle版本1利用该类的第1版,而client版本2利用该类的第2版。这在 模块化系统中相当普遍;在同一个VM中,有些代码大概需要装载一个类库的老版 本,同时更新点的代码(在另一个bundle中)却需要该类库的新版本。亏得OSGi 为你打点起这种依赖通报,确保不再呈现不兼容类激发的问题。
类的垃圾接纳
每个类都有一个对其类装载器的引用。因此假如想要从差异的bundle会见这 些类,不单要有对该类实例的引用,并且还要有对该类的类装载器的引用。当一 个bundle持有另一个bundle的类时,它也会将该bundle牢靠在内存中。在前篇文 章的例子中,client被牢靠到该server上。
在静态世界里,无论你是否把本身的类牢靠到其他类(或类库)都无所谓; 因为不会有什么变革。但是,在动态世界里,在运行时将类库或东西替换成新版 本就有大概了。这听起来大概有点巨大,可是在可热陈设应用的Web应用引擎早 期就呈现了(如Tomcat,最早宣布于1999年)。每个Web应用措施都绑定到 Servlet API的某个版本上,当其遏制时,装载该Web应用的类加载器也就废弃掉 了。当Web应用从头被陈设时,又建设了一个新的类加载器,新版类就由其装载 。只要servlet引擎没有保持对老版应用的引用,这些类就像其他Java工具一样 被垃圾接纳器接纳了。
并不是所有的类库都能意识到Java代码中大概存在类泄漏的问题,就像是内 存泄漏。一个典范的例子就是Log4J的addAppender()挪用,一旦其执行了,将会 把你的类绑定在Log4J bundle的生命周期上。纵然你的bundle遏制了,Log4J仍 将维对appender的引用,并继承发送日志事件(除非该bundle在遏制时恰内地调 用了removeAppender()要领)。
查找和绑定
为了成为动态,我们需要有一个能查找处事的机制,而不是耐久持有他们( 以免bundle遏制)。这是通过利用简朴Java接口和POJO来实现的,也就是各人所 熟知的services(留意他们与WS-DeathStar或其他任何XML底层架构都没有干系 ;他们就是普通Java工具——Plain Old Java Objects)。
典范工场实现方法是利用从properties文件中获取的某种形式的类名,然后 用Class.forName()来实例化相应的类,OSGi则差异,它 维护了一个‘处事注册 器’,其实这是一个包括了类名和处事的映射列表。这样,OSGi系统就可以利用 context.getService(getServiceReference("java.sql.Driver")),而不是 class.forName("com.example.JDBCDriver")来获取一个JDBC驱动器。这就把 client代码解放出来了,它不需知道任何特定客户端实现;相反,它可以在运行 时绑定任何可用驱动措施。移植到差异的数据库处事器也就很是简朴了,只需停 止一个模块并启动一个新模块;client甚至不需要从头启动,也不需要改变任何 设置。
#p#分页标题#e#
这样做是因为client只需知道其所需的处事的API(根基上都是接口,尽量 OSGi类型答允利用其他类)。在上述环境中,接口名是 java.sql.Driver;返回 的接话柄例是详细的数据库实现(不必相识是哪些类,编码在哪里)。另外,如 果处事不行用(数据库不存在,或数据库姑且停掉了),那么这个要了解返回 null以说明该处事不行用。
为了完全动态,返回功效不该被缓存。换句话说,每当需要处事的时候,需 要从头挪用getService。框架会在底层执行缓存操纵,因此不存在太大的机能 问题。但重要的是,它答允数据库处事在线被替换成新的处事,假如没有缓存代 码,那么下次挪用时,client将透明地绑定到新处事上。
#p#副标题#e#
付诸实施
为了证明这一点,我们将建设一个用于缩写URL的OSGi处事。其思路是处事接 收一个长URL,如http://www.infoq.com/articles/modular-java-what-is-it, 将其转换为短点的URL,如http://tr.im/EyH1。该处事也可以被遍及应用在 Twitter这样的站点上,还可以用它来把长URL转成短的这样便签背后也写得下。 甚至像《新科学家》和《Macworld》这样的杂志也是用这些短URL来印刷媒体链 接的。
为了实现该处事,我们需要:
一个缩写处事的接口
一个注册为缩写实现的bundle
一个验证用client
尽量并没有克制把这些对象都放在同一个bundle中,可是我们照旧把他们分 别放在差异的bundle里。(即便他们在一个bundle中,最好也让bundle通过处事 来通讯,就仿佛他们处于差异的bundle一样;这样他们就可以利便地与其他处事 提供者举办集成。
把缩写处事接口与其实现(或client)分隔放在单独bundle中是很重要的。 该接口代表了client和server之间的‘共享代码’,这样,该接口在每个bundle 中城市加载。正因如此,每个bundle实际上都被牢靠到了该接口特定版本上,所 有处事都有配合的生命周期,将接口放在单独 bundle中(在整个OSGi VM生命周 期中都在运行),我们的client就可以自由变革。假如我们把该接口放在某个服 务实现的bundle中,那么该处事产生变革后我们就不能从头毗连到client上了。
shorten接口的manifest和实现如下:
Bundle-ManifestVersion: 2
Bundle-Name: Shorten
Bundle-SymbolicName: com.infoq.shorten
Bundle-Version: 1.0.0
Export-Package: com.infoq.shorten
---
package com.infoq.shorten;
public interface IShorten {
public String shorten(String url) throws IOException;
}
上面的例子成立了一个拥有单一接口(com.infoq.shorten.IShorten)的 bundle(com.infoq.shorten),并将其输出给client。参数是一个URL,返回一 个独一的压缩版URL。
和接口界说对比,实现就相对有趣一些了。尽量最近缩写名称的应用开始多 起来了,可是所有这些应用的祖师爷都是 TinyURL.com。(具有嘲讽意味的是, http://tinyurl.com实际上可以被压缩的更短http://ow.ly/AvnC)。如今较量 风行有:ow.ly、bit.ly、tr.im等等。这里并不是对这些处事全面先容,也不是 为其背书,我们的实现也可以利用其他同类处事。本文之所以利用TinyURL和 Tr.im,是由于他们都可以匿名基于GET提交,易于实现,除此之外没有其他原因 。
每种实现实际上都很是小;都以URL为参数(要缩写的对象)并返回新的压缩 过的文本:
package com.infoq.shorten.tinyurl;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import com.infoq.shorten.IShorten;
public class TinyURL implements IShorten {
private static final String lookup =
"http://tinyurl.com/api-create.php?url=";
public String shorten(String url) throws IOException {
String line = new BufferedReader(
new InputStreamReader(
new URL(lookup + url).openStream())).readLine();
if(line == null)
throw new IllegalArgumentException(
"Could not shorten " + url);
return line;
}
}
Tr.im的实现雷同,只需用http://api.tr.im/v1/trim_simple?url=替代 lookup的值即可。这两种实现的源代码别离在com.infoq.shorten.tinyurl和 com.infoq.shorten.trim bundle里。
那么,完成缩写处事的实现后,我们如何让其他措施会见它呢?为此,我们 需要把实现注册为OSGi框架的处事。BundleContext类的registerService (class,instance,properties)要领可以让我们界说一个处事以供后用,该要领 凡是在bundle的start()挪用期间被挪用。如上篇文章所讲,我们必需界说一个 BundleActivator。实现该类后,我们还要把Bundle-Activator放在MANIFEST.MF 里以便找到该实现。代码如下:
#p#分页标题#e#
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: TinyURl
Bundle-SymbolicName: com.infoq.shorten.tinyurl
Bundle-Version: 1.0.0
Import-Package: com.infoq.shorten,org.osgi.framework
Bundle-Activator: com.infoq.shorten.tinyurl.Activator
---
package com.infoq.shorten.tinyurl;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import com.infoq.shorten.IShorten;
public class Activator implements BundleActivator {
public void start(BundleContext context) {
context.registerService(IShorten.class.getName(),
new TinyURL(),null);
}
public void stop(BundleContext context) {
}
}
尽量registerService()要领吸收一个字符串作为其第一个参数,并且 用"com.infoq.shorten.IShorten"也是可以的,可是最好照旧用 class.class.getName()这种形式,这样假如你重构了包或改变了类名,在编译 时就可发明问题。假如用字符串,举办了错误的重构,那么只有在运行时你才气 知道问题地址。
registerService()的第二个参数是实例自己。之所以将其与第一个参数分隔 ,是因为你可以将同一个处事实例输出给多个处事接口(假如需要带有版本的 API,这就有用了,你可以进化接口了)。别的,一个bundle输出同一范例的多 个处事也是有大概的。
最后一个参数是处事属性(service properties)。答允你给处事加上特别 元数据注解,好比标注优先级以表白该处事相对付其他处事的重要性,可能挪用 者体贴的其他信息(好比成果描写和厂商)。
只要该bundle一启动,缩写处事就可用了。当bundle遏制,框架将自动打消 处事注册。假如我们想要本身打消注册(例如说,对错误代码和网络接口不行用 所作出的响应)也很容易(用context.unregisterService())。
利用处事
一旦处事起来并运行之后,我们就可以用client会见它了。假如运行的是 Equinox,你可以用services呼吁摆列所有已安装的处事,以及它们是由谁注册 的:
{com.infoq.shorten.IShorten}={service.id=27}
Registered by bundle: com.infoq.shorten.trim-1.0.0 [1]
No bundles using service.
{com.infoq.shorten.IShorten}={service.id=28}
Registered by bundle: com.infoq.shorten.tinyurl-1.0.0 [2]
No bundles using service.
在挪用处事处理惩罚URL之前,client需要理会处事。我们需要得到一个处事引用 ,它可以让我们查察处事自身内部的属性,然后操作其来得到我们感乐趣的处事 。但是,我们需要可以或许反复处理惩罚沟通及差异的URL,以便我们可以把它集成到 Equinox或Felix的shell里。实现如下:
package com.infoq.shorten.command;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import com.infoq.shorten.IShorten;
public class ShortenCommand {
protected BundleContext context;
public ShortenCommand(BundleContext context) {
this.context = context;
}
protected String shorten(String url) throws IllegalArgumentException, IOException {
ServiceReference ref =
context.getServiceReference(IShorten.class.getName());
if(ref == null)
return null;
IShorten shorten = (IShorten) context.getService(ref);
if(shorten == null)
return null;
return shorten.shorten(url);
}
}
当shorten要领被挪用时,上面这段措施将查找处事引用并得到处事工具。然 后我们可以把处事工具赋值给一个IShorten工具,并利用它与前面讲到的已注册 处事举办交互。留意这些都是在同一个VM中产生的;没有长途挪用,没有强制异 常,没有参数被序列化;只是一个POJO与另一个POJO对话。实际上,这里与最开 始class.forName()例子的独一区别是:我们如何得到shorten POJO。
为了在Equinox和Felix内里利用这一处事,我们需要放一些样板代码进去。 必需提一下,当我们界说manifest时,我们可以在 Felix和 Equinox呼吁行上声 明可选依赖,这样,当我们两者中任何一个安装之后,我们就可以运行了。(一 个更好的办理方案是将其陈设为单独的bundles,这样我们可以去掉选项;可是 假如bundle不存在,activator将会失败,因此无法启动)。Equinox和Felix特 定数令的源代码在com.infoq.shorten.command bundle中。
#p#分页标题#e#
假如我们安装了呼吁client bundle,我们将获得一个新呼吁,shorten,通 过OSGi shell可以挪用它。要运行该呼吁,需要先执行java -jar equinox.jar -console -noExit或java -jar bin/felix.jar,然后安装bundle,之后你就可 以利用该呼吁了:
java -jar org.eclipse.osgi_* -console -noExit
osgi> install file:///tmp/com.infoq.shorten-1.0.0.jar
Bundle id is 1
osgi> install file:///tmp/com.infoq.shorten.command- 1.0.0.jar
Bundle id is 2
osgi> install file:///tmp/com.infoq.shorten.tinyurl- 1.0.0.jar
Bundle id is 3
osgi> install file:///tmp/com.infoq.shorten.trim-1.0.0.jar
Bundle id is 4
osgi> start 1 2 3 4
osgi> shorten http://www.infoq.com
http://tinyurl.com/yr2jrn
osgi> stop 3
osgi> shorten http://www.infoq.com
http://tr.im/Eza8
留意,在运行时TinyURL和Tr.im处事都是可用的,可是一次只能利用一种服 务。可以配置一个处事级别(service ranking),这是一个整数,取值范畴在 Integer.MIN_VALUE和Integer.MAX_VALUE之间,当处事最初注册时给 Constants.SERVICE_RANKING赋予相应值。值越大暗示级别越高,当需要处事时 ,会返回第一流此外处事。假如没有处事级别(默认值为 0),可能多个处事的 处事级别沟通,那么就利用自动分派的Constants.SERVICE_PID,大概是任意顺 序的一个处事。
另一个需留意的问题是:当我们遏制一个处事时,client会自动失败转移到 列表中的下一个处事。每当该呼吁执行时,它会获取(当前)处事来处理惩罚 URL压 缩需求。假如在运行期间处事提供措施产生了变革,不会影响呼吁的利用,只要 有此需求时有处事在就成。(假如你遏制了所有处事提供措施,处事查找将返回 null,这将会打印出相应的错误信息——好的代码应该确保措施可以或许防范返回服 务引用为null的环境产生。)
处事跟踪
除过每次查询处事外,还可以用ServiceTracker来取代做这一事情。这就跳 过了中间得到ServiceReference的几步,可是要求你在结构之后挪用open,以便 开始跟踪处事。
对付ServiceReference,可以挪用getService()得到处事实例。而 waitForService()则在处事不行用时阻塞一段时间(按照指定的timeout。假如 timeout为0,则永远阻塞)。我们可以如下从头实现shorten呼吁:
package com.infoq.shorten.command;
import java.io.IOException;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.infoq.shorten.IShorten;
public class ShortenCommand {
protected ServiceTracker tracker;
public ShortenCommand(BundleContext context) {
this.tracker = new ServiceTracker(context,
IShorten.class.getName(),null);
this.tracker.open();
}
protected String shorten(String url) throws IllegalArgumentException,
IOException {
try {
IShorten shorten = (IShorten)
tracker.waitForService(1000);
if (shorten == null)
return null;
return shorten.shorten(url);
} catch (InterruptedException e) {
return null;
}
}
}
利用Service Tracker的常见问题是在结构后健忘了挪用open()。除此之外, 还必需在MANIFEST.MF内部引入org.osgi.util.tracker包。
利用ServiceTracker来打点处事依赖凡是被认为是打点干系的好要领。在没 有利用处事的环境下,查找已输出的处事稍微有点巨大:好比, ServiceReference在其被理会为一个处事之前溘然变得不行用了。存在一个 ServiceReference的原因是,沟通实例可以或许在多个bundle间共享,并且它可以被 用来基于某些尺度(手工)过滤处事。并且,它还可以利用过滤器来限制可用服 务的荟萃。
处事属性和过滤器
当一个处事注册时,可以将处事属性一起注册。大多环境部属性可觉得null ,可是也可以提供OSGi特定或关于URL的通用属性。譬喻,我们想给处事分级以 便区分优先级。我们可以注册Constants.SERVICE_RANKING(代表优先级的数值 ),作为最初注册进程的一部门。我们大概还想放一些 client想知道的元数据 ,好比处事的主页在哪儿,该站点的条款链接。为达此目标,我们需要修改 activator:
public class Activator implements BundleActivator {
public void start(BundleContext context) {
Hashtable properties = new Hashtable();
properties.put(Constants.SERVICE_RANKING, 10);
properties.put(Constants.SERVICE_VENDOR, "http://tr.im");
properties.put("home.page", "http://tr.im");
properties.put("FAQ", "http://tr.im/website/faqs");
context.registerService(IShorten.class.getName(),
new Trim(), properties);
}
...
}
#p#分页标题#e#
处事级别自动由ServiceTracker及其他工具来打点,但也可以用特定条件来 过滤。Filter是由LDAP气势气魄的过滤器改编而来的,其利用了一种前缀暗示法 (prefix notation)来执行多个过滤。固然大都环境下你想提供类的名字 (Constants.OBJECTCLASS),但你也可以对值举办检讨(包罗限制持续变量的 取值范畴)。Filter是通过BundleContext建设的;假如你想跟踪实现了 IShorten接口的处事,而且界说一个FAQ,我们可以这样做:
...
public class ShortenCommand
public ShortenCommand(BundleContext context) {
Filter filter = context.createFilter("(&" +
"(objectClass=com.infoq.shorten.IShorten)" +
"(FAQ=*))");
this.tracker = new ServiceTracker(context,filter,null);
this.tracker.open();
}
...
}
在界说处事时可以被过滤或可以配置的尺度属性包罗:
service.ranking (Constants.SERVICE_RANKING) – 整数,可以区分处事优 先级
service.id (Constants.SERVICE_ID) – 整数,在处事被注册时由框架自动 配置
service.vendor (Constants.SERVICE_VENDOR) – 字符串,表白处事出自谁 手
service.pid (Constants.SERVICE_PID) – 字符串,代表处事的PID (persistent identifier)
service.description (Constants.SERVICE_DESCRIPTION) – 处事的描写
objectClass (Constants.OBJECTCLASS) – 接口列表,处事被注册在哪些接 口下
过滤器语法在OSGi焦点类型的 3.2.7节 “Filter syntax”中有界说。最基 本的,它答允如便是(=)、约便是(~=)、大于便是、小于便是以及子字符串 较量等操纵符。括号将过流器分组,而且可以团结利用“&”、“|” 或“! ”别离代表and、or和not。属性名不是巨细写敏感的,值大概是(假如不消~=作 比的话)。“*”是通配符,可用来支持子字符串匹配,好比 com.infoq.*.*。
总结
本文中,我们先容了如何利用处事举办bundle间通信,以替代直接类引用的 要领。处事可以让模块系统动态化,这样就能应对在运行时处事的变革问题。我 们还打仗到了处事级别、属性及过滤器,并利用尺度处事跟踪器来更容易的会见 处事并跟踪变革的处事。我们将在下一部门先容如何用声明式处事使得处事的编 写越发容易。