java 模块化
在模块化Java系列的第3部分中,我们将介绍动态模块化 。 我们将描述如何解决捆绑商品的类,它们如何来去去以及它们如何彼此通信。
上一期“ 模块化Java:静态模块化 ”描述了如何将Java模块构建和部署为单独的JAR。 该示例提供了客户端和服务器捆绑包(都在同一VM中),并且客户端通过工厂方法找到了服务器。 在那个例子中,工厂实例化了一个已知的类,但是同样可以使用反射来获得服务实现。 Spring大量使用此技术将弹簧对象绑定在一起。
在介绍动态服务之前,值得退一步考虑类路径,因为标准Java代码和模块化Java代码之间的区别之一是在运行时如何绑定依赖项。 讲完这些内容后,我们将简要介绍类的垃圾回收; 因此,如果对此感到满意,则可以跳过 。
捆绑类路径
在一个平面Java程序中,只有一个类路径-应用程序启动时使用的类路径。 通常在命令行上使用-classpath
或通过CLASSPATH
环境变量来指定。 然后,当尝试在运行时解析类时,无论是静态(编译为代码)还是动态(使用反射和class.forName()
),Java类加载器都会扫描此路径。 但是,可以在运行时使用多个类加载器。 诸如Jetty和Tomcat之类的Web应用程序引擎经常使用此功能来支持应用程序的热(重新)部署。
在OGSi中,每个捆绑包都有自己的类加载器。 从其他捆绑软件访问的类将委派给其他捆绑软件的类加载器。 因此,在传统应用程序中,来自日志记录库,客户端和服务器JAR的类可能由同一个类加载器加载,而在OSGi模块系统中,每个都将由它们自己加载。
一个必然的结论是,VM中可能有多个具有相同名称的不同Class
对象的类加载器。 可以在同一VM中通过捆绑com.infoq.example
的版本1和版本2导出名为com.infoq.example.App
的类。 绑定到版本1的客户端捆绑将获得版本1类,而绑定到版本2的客户端将获得版本2类。 对于模块化系统,这是相当普遍的情况。 某些代码可能需要加载库的旧版本,而较新的代码(在另一个捆绑包中)可能需要在同一VM中加载库的新版本。 幸运的是,OSGi可以为您管理此类传递依赖项,并确保您永远不会因类不兼容而出现问题。
垃圾分类
每个类都有对定义它的类加载器的引用。 因此,如果要从其他捆绑软件访问类,则不仅要明确引用该类(的实例),还要明确引用其类加载器。 虽然一束持有到其他的类,它引脚捆绑到内存中。 在前面的示例中,客户端固定到服务器。
在静态世界中,将自己固定到另一个类(或库)并不重要。 没有来去去去。 但是,在动态世界中,有可能在运行时将库或实用程序替换为较新的版本。 这听起来可能很复杂,但是实际上,自从像Tomcat(于1999年首次发布)之类的Web应用程序引擎问世以来,就一直在进行Web应用程序的热部署。 每个Web应用程序都绑定到Servlet API的版本,并且在停止时,将删除加载Web应用程序的类加载器。 重新部署Web应用程序时,将创建新的类加载器,并加载新版本的类。只要Servlet引擎不尝试保留来自旧应用程序的引用,这些类就会被垃圾回收。就像其他任何Java对象一样。
并非所有的库都知道类泄漏的可能性,例如内存泄漏,可以用Java编写代码。 一个明显的例子是Log4J的addAppender()
调用,该调用一旦执行,便会将您的类绑定到Log4J捆绑软件的生命周期中。 即使捆绑软件已停止,Log4J也会维护对附加程序的引用,并继续发送日志事件(除非捆绑软件在停止时调用适当的removeAppender()
方法)。
查找和绑定
为了保持动态,我们需要一种机制来查找服务,但不能永久保留它们(以防捆绑消失)。 这是通过使用简单的Java接口和POJO(称为services)来实现的 。 (请注意,它们与WS-DeathStar或任何其他XML繁重的基础结构没有任何关系;它们只是普通的旧Java对象。)
典型的工厂将使用通过属性文件和随后的Class.forName()
获取的某种形式的类名来实现,而OSGi维护一个“服务注册表”,实质上是一个包含类名和服务列表的映射。 因此,OSGi系统可以使用context.getService(getServiceReference("java.sql.Driver"))
代替使用class.forName("com.example.JDBCDriver")
获取JDBC驱动程序。 这使客户端代码无需知道任何特定的客户端实现; 相反,它可以绑定到运行时可用的任何驱动程序。 如果要停止一个模块并启动一个新模块,则需要迁移到其他数据库服务器。 客户端甚至不需要重新启动,也不需要更改任何配置。
这样做的原因是客户端仅需要知道其所请求服务的API。 尽管OSGi规范允许使用任何类,但这几乎总是一个接口。 在上述情况下,接口名称为java.sql.Driver
; 返回的接口实例是数据库实现(其类未知或实际上在任何地方都已编码)。 此外,如果该服务不可用(没有数据库,或者数据库已被临时停止),则此方法返回null
指示没有此类服务可用。
为了完全动态,不应缓存返回结果。 换句话说,每次需要服务时,都需要重新调用getService
。 该框架在后台执行缓存,因此这并不是性能问题。 但重要的是,它允许在不更改代码的情况下用新服务即时替换数据库服务–在下一次调用时,客户端将透明地绑定到新服务。
付诸行动
为了证明这一点,我们将创建一个OSGi服务,该服务可用于缩短URL。 这些(大部分可互换的)服务的想法是采用一个长URL,例如http://www.infoq.com/articles/modular-java-what-is-it ,并将其转换为一个较短的URL,例如http: //tr.im/EyH1 。 除了在Twitter之类的网站上广泛使用之外,还可以使用它们将原本较长的URL复制到可以写在便笺背面的内容中。 甚至《新科学家》和Macworld等杂志也将它们用于印刷媒体链接。
为了实施服务,我们需要:
- 缩短服务的接口
- 一个捆绑包,启动后将注册一个简化的实现
- 示范客户
尽管没有什么可以阻止所有这些都放在同一个包中的,但我们将它们放在单独的包中。 (即使它们在同一捆绑中,最好的做法是允许捆绑通过服务进行通信,就像它们在单独的捆绑中一样;这使它们更易于与其他提供程序集成。)
缩短服务的接口与任何实现(或客户端)都在单独的捆绑包中,这一点很重要。 该接口表示客户端和服务器之间的“共享代码”,因此每个捆绑包都将其加载。 由于这有效地将每个捆绑包固定到(特定版本的)接口以实现所有服务的集体生命周期,因此可以将其放置在单独的捆绑包中(该捆绑包将在OSGi VM的整个生命周期内保持运行),我们可以允许客户来来去去。 如果我们将接口与服务实现之一放在同一捆绑中,那么当该服务来来去去时,我们将无法重新连接客户端。
short接口的清单和实现不是那么有趣:
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
)设置捆绑包( com.infoq.shorten
),然后将其导出到客户端。 该参数将仅使用URL,然后返回该URL的缩写版本。
实现有点有趣。 它们的祖父都是TinyURL.com ,尽管最近,简短的名字开始出现。 (具有讽刺意味的是,实际上http://tinyurl.com可以缩写为较小的URL,例如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的实现与之类似,除了使用URL http://api.tr.im/v1/trim_simple?url=
作为查找之外。 两者的源都在com.infoq.shorten.tinyurl和com.infoq.shorten.trim捆绑包中。
那么,在实现缩短服务的前提下,我们如何使其他人可以访问它呢? 好吧,我们需要将其注册为OSGi框架的服务。 BundleContext
类上的registerService( class , instance , properties )
方法允许我们定义一个服务以供以后使用,通常在bundle的start()
调用期间调用。 如上次所述 ,这意味着我们必须定义一个BundleActivator
。 除了该类的实现之外,我们还必须记住将Bundle-Activator
放入MANIFEST.MF
中以查找实现。 它将是这样的:
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()
,就像重构class .class.getName()
一样。打包或更改类名,它将被编译器捕获。 如果只使用字符串,并且执行了错误的重构,那么直到运行时您才知道该问题。
registerService()
的第二个参数是实例本身。 这与第一个参数不同的原因是,您可以使同一服务实例导出多个服务接口(如果您的要求中具有版本化的API,则很有用,因为您可以扩展接口 )。 另外,单个捆绑包很有可能会导出相同类型的多个服务。
最后一个参数用于服务属性 。 这些允许您使用额外的元数据来注释服务,例如,指示对该服务相对于其他服务的重要性的偏好,或调用者可能感兴趣的其他信息(例如,描述和供应商)。
此捆绑包启动后,缩短服务将提供给客户。 当捆绑包停止时,框架将自动取消注册任何服务。 如果我们愿意(例如,响应错误代码或网络接口不可用),我们可以更早地取消注册(使用context.unregisterService()
)。
使用服务
服务启动并运行后,我们便可以使用客户端来访问它。 如果您在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调用该服务。 我们需要获取一个服务引用 ,该引用使我们可以对服务本身的属性进行内省,然后使用它来获取我们感兴趣的服务 。但是,我们将需要能够重复执行此操作(并使用不同的网址),因此我们可以将其集成到Equinox或Felix外壳中。 这是实现的作用:
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
转换为IShorten
对象,并使用它与先前注册的服务进行交互。 请注意,所有这些都在同一个VM中。 没有远程调用,没有强制性例外,没有参数被序列化; 只是一个POJO与另一个POJO交谈。 实际上,此示例与初始class.forName()
示例之间的唯一区别是我们如何获取shorten
POJO。
为了在Equinox和Felix中使用此代码,我们需要添加一些样板代码。 可以说,当我们定义清单时,我们可以在Felix和Equinox命令行界面上声明可选的依赖项,这样,当我们在其中一个安装时,我们就可以运行。 (一个更好的解决方案可能是将它们部署为单独的捆绑包,这样我们就可以失去可选性;但是如果捆绑包不存在,则激活器将失败,因此将无法启动。)特定于Equinox和Felix的资源命令联播位于com.infoq.shorten.command捆绑包中,以示好奇。
结果是,如果我们安装命令客户端捆绑包,我们将得到一个新命令shorten
,可以从OSGi shell调用该命令。 如果运行java -jar equinox.jar -console -noExit
或java -jar bin/felix.jar
则会运行此java -jar bin/felix.jar
。 您需要执行捆绑软件的安装才能工作。 之后,您将可以使用以下命令:
java -jar org.eclipse.osgi_* -console -noExit osgi> installfile:///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服务在运行时都可用,但显然一次只能使用一个服务。 可以在首次注册服务时通过使用Constants.SERVICE_RANKING
键输入一个相应的值来设置服务等级 (它是Integer.MIN_VALUE
和Integer.MAX_VALUE
之间的整数)。 较高的值表示较高的排名,当查询服务时,将返回排名最高的服务。 在没有服务排名(默认为零)的情况下,或者在与多个服务发生平局的情况下,自动分配的Constants.SERVICE_PID
可用于对服务进行任意排序。
要注意的另一件事是,当我们停止其他服务时,客户端会自动故障转移到列表中的下一个服务。 每次运行该命令时,它都会获得(当前)服务以用于其缩短需求。 如果服务提供商在两次运行之间进行更改,则无需关心该命令,只需在需要时使用该命令即可。 (如果停止所有提供程序,则服务查找将返回null
,这将导致打印错误-良好的代码应确保其进行防御性编程,以防止为服务参考返回null
的可能性。
服务追踪器
不必每次都查找服务,而可以使用ServiceTracker
来完成工作。 这跳过了获取ServiceReference
的中间步骤,但确实需要您在构造后调用open
,以便开始跟踪服务。
与ServiceReference
,可以调用getService()
获得服务实例。 还有waitForService()
,如果在指定的超时时间内服务不可用(或者如果超时为零,则永远阻塞waitForService()
,它将阻塞。 我们可以重新执行缩短命令,如下所示:
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()
便会忘记调用它。 为了运行,还需要将org.osgi.util.tracker
作为包导入到MANIFEST.MF中。
使用ServiceTracker
来管理对服务的依赖关系通常被视为管理关系的一种好方法。 在不使用服务时(例如,当ServiceReference
变得不可用但在将其解析为服务之前),在查找服务时会发现一些细微的复杂性。 具有ServiceReference
的基本原理是,可以在捆绑包中共享同一实例,并且可以将其用于基于某些条件(手动)过滤出服务。 但是,也可以使用过滤器来限制可用服务的集合。
服务属性和过滤器
注册服务后,可以向其注册服务属性。 在大多数情况下,它可以为null
,但是可以提供有关URL的OSGi特定属性和常规属性。 例如,假设我们要按优先级对服务进行排名。 在初始注册过程中,我们可以使用某种形式的数字首选项值来注册Constants.SERVICE_RANKING
。 我们可能还希望放入一些客户可能希望知道的元数据,例如服务的主页在哪里,以及指向该网站的条款和条件的链接。 为此,我们需要修改我们的激活器:
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); } ... }
服务排名由ServiceTracker
和其他人员自动管理,但是也可以过滤出具有某些属性的服务。 该Filter
是从LDAP样式过滤Filter
编译而成的,该过滤器使用前缀表示法来执行多个过滤器。 尽管最常见的是提供类的名称( Constants.OBJECTCLASS
),但是您也可以对值进行测试(甚至限制连续变量的范围)。 过滤器是通过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)-一个字符串或字符串数组,表示服务的持久标识符 -
service.description
(Constants.SERVICE_DESCRIPTION)-服务描述 -
objectClass
(Constants.OBJECTCLASS)-此服务在其下注册的接口列表
过滤器语法在OSGi核心规范的第3.2.7节“过滤器语法”中定义 。 本质上,它允许诸如等于(=),逼近(〜=),大于等于,小于等于等操作以及子字符串比较。 括号将过滤器分组,并且可以与&,|组合使用 要么 ! 和或否的修饰符。 尽管属性名称不区分大小写,但值可以是(除非与〜=相比)。 *字符用于表示通配符,并且可以用于支持com.infoq.*.*
子字符串匹配。
摘要
在本文中,我们探讨了如何使用服务作为包之间的通信方式,而不是直接的类引用。 服务允许模块系统是动态的,以便它对运行时来来往往的服务做出React。 我们还涉及了服务等级,属性和过滤器,并使用了标准的服务跟踪器,以使其易于访问和跟踪来来往往的服务。 在下一部分中,我们将研究如何通过声明式服务使服务的连接更容易。
可安装的捆绑软件(也包含源代码):
- com.infoq.shorten-1.0.0.jar
- com.infoq.shorten.command-1.0.0.jar
- com.infoq.shorten.trim-1.0.0.jar
- com.infoq.shorten.tinyurl-1.0.0.jar
java 模块化