java 模块化_模块化Java:动态模块化

java 模块化

在模块化Java系列的第3部分中,我们将介绍动态模块化 。 我们将描述如何解决捆绑商品的类,它们如何来去去以及它们如何彼此通信。

上一期“ 模块化Java:静态模块化 ”描述了如何将Java模块构建和部署为单独的JAR。 该示例提供了客户端和服务器捆绑包(都在同一VM中),并且客户端通过工厂方法找到了服务器。 在那个例子中,工厂实例化了一个已知的类,但是同样可以使用反射来获得服务实现。 Spring大量使用此技术将弹簧对象绑定在一起。

在介绍动态服务之前,值得退一步考虑类路径,因为标准Java代码和模块化Java代码之间的区别之一是在运行时如何绑定依赖项。 讲完这些内容后,我们将简要介绍类的垃圾回收; 因此,如果对此感到满意,则可以跳过

捆绑类路径

在一个平面Java程序中,只有一个类路径-应用程序启动时使用的类路径。 通常在命令行上使用-classpath或通过CLASSPATH环境变量来指定。 然后,当尝试在运行时解析类时,无论是静态(编译为代码)还是动态(使用反射和class.forName() ),Java类加载器都会扫描此路径。 但是,可以在运行时使用多个类加载器。 诸如JettyTomcat之类的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.lybit.lytr.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.tinyurlcom.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 -noExitjava -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_VALUEInteger.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。 我们还涉及了服务等级,属性和过滤器,并使用了标准的服务跟踪器,以使其易于访问和跟踪来来往往的服务。 在下一部分中,我们将研究如何通过声明式服务使服务的连接更容易。

可安装的捆绑软件(也包含源代码):

翻译自: https://www.infoq.com/articles/modular-java-dynamic-modularity/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

java 模块化

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值