本节更深入地介绍了 JBoss Cache 的架构,它适用于希望使用更高级的缓存功能、扩展或增强缓存、编写插件或了解底层运行机制的使用着。
JBossCache 由以树型结构组织的 Node 实例集合组成。每个 Node 都包含一个保存要缓存的数据对象的表。请注意,这个结构是一种数学树,并非图形;每个 Node 都有且只有一个父节点,且根节点由一个不变的全限定名 Fqn.ROOT 表示。
在上面的图表里,每个方块都代表一个 JVM。你可以看到两个缓存位于不同的 JVM 里,彼此复制数据。在其中一个缓存里的任何修改都将复制到另外一个缓存里。自然,集群系统可以有多个缓存。根据事务性设置,复制将在每次修改发生后或事务结束后(提交时)进行。当新的缓存被创建时,它可以在启动时获取某个现有缓存的内容。
除了 Cache 和 Node 接口,JBossCache 也开放更强大的 CacheSPI 和 NodeSPI 接口,它们提供对 JBossCache 内部更多的控制。这些接口不是用于普通用途,它们适用于扩展和增加 JBossCache 、编写自定义的拦 截 器 (Interceptor) 或类加载器 (CacheLoader) 实例。
CacheSPI接口无法被创建,但它靠这些接口的 setCache(CacheSPI cache) 方法注入到Interceptor 和 CacheLoader 实现里。CacheSPI 继承了Cache,所以基本 API 的所有功能都是可用的。类似地,NodeSPI接口也无法被创建。相反,它是通过执行 CacheSPI 上的操作来获得的。例如,Cache.getRoot() : Node 被覆盖为 CacheSPI.getRoot() : NodeSPI。
请注意,既然接口继承不是能够保证向前维持的合约,我们不推荐直接转换 Cache 或Node 为其 SPI 对应接口,这是不好的做法。从另外一方面来讲,开放的公用 API 是保证可以维持的。
既然缓存基本上是一个节点的集合,当作为整体或单个节点调用缓存上的操作时,集群、持久化、逐出等方面都需要应用到这些节点上。要以一种清洁、模块化和可扩展的方式实现这一点,我们使用了一种拦截器链。这个链由一系列的拦截器组成,每个都添加了一种方面或特定的功能。当缓存创建时,这个链将基于所用的配置构建。
请注意,NodeSPI 提供一些直接在节点上操作而无需通过拦截器链的方法(如 xxxDirect() 方法族)。插件作者应该注意到使用这样的方法将影响到缓存可能需要应用的方面,如锁、复制等。为简便起见,请不要使用这些方法,除非你真的知道自己在干什么。
JBossCache 基本上是一个核心的数据结构 - 对 DataContainer 的实现 - 方面和功能在此数据结构之上用拦截器实现。CommandInterceptor 是一个抽象类,拦截器实现继承了它。CommandInterceptor 实现了 Visitor 接口,所以它能够以一种强类型的方式修改命令。下节将介绍关于 Visitor 和 Command 的更多内容。拦截器实现在 InterceptorChain 类里被链接在一起,它在整个链里分发一个命令。CallInterceptor 是一个特殊的拦截器,它总是位于链的末端来调用通过 process() 方法传递的命令。JBossCache 附带几个拦截器,代表着不同的行为方面,例如:
针对你的缓存实例配置的拦截器链可以通过调用 CacheSPI.getInterceptorChain() 获得和检查,这个方法返回一个已排序的拦截器List,它是以命令将遇到的顺序进行排序的。
当然我们也可以编写自定义的拦截器,通过继承CommandInterceptor 并基于你感兴趣拦截的命令来覆盖相关的 visitXXX() 方法,你可以编写自定义的拦截器以添加特殊的方面或功能。你也可以继承一些其他的抽象拦截器,如PrePostProcessingCommandInterceptor和SkipCheckChainedInterceptor。关于其他功能的细节,请参考相关的 Javadoc。自定义拦截器需要使用Cache.addInterceptor() 方法添加到拦截器链中。JBossCache 也支持通过XML 配置自定义拦截器。
JBossCache 在内部使用一种command/visitor 模式来执行 API 类。每当在缓存接口上调用一个方法,实现了 Cache 接口的CacheInvocationDelegate 将创建一个 VisitableCommand 实例并将这个命令分发到拦截器链里。而实现了Visitor 接口的拦截器能够处理它们所感兴趣的 VisitableCommand 并添加行为到这个命令里。
每个命令都包含了正在执行的命令的全部知识,如所使用的参数和封装在process() 方法里的处理行为。例如,当调用 Cache.removeNode() 时,RemoveNodeCommand 将被创建并传递到拦截器链里,而 RemoveNodeCommand.process() 知道如何从数据结构里删除节点。
除了可被访问以外,命令也是可以复制的。JBossCache marshaller 知道如何高效地将命令编码并使用内部的基于 JGroups 的RPC 机制在远程缓存实例上调用它们。
InvocationContext保留着单次调用期间的中间状态,且由位于拦截器链前端的 InvocationContextInterceptor 设置和销毁。InvocationContext,顾名思义,持有和单次方法调用相关联的上下文信息。上下文信息包含相关联的 javax.transaction.Transaction 或 org.jboss.cache.transaction.GlobalTransaction、方法调用起始者(InvocationContext.isOriginLocal())、以及被锁定的节点相关的信息等。InvocationContext 可以通过调用 Cache.getInvocationContext() 获取。
有些方面和功能是由多个拦截器共享的。其中一些已经封装成管理者,由不同的拦截器所使用,且通过 CacheSPI 接口被访问。
JBossCache 的早期版本只是简单地通过 ObjectOutputStream 在复制时将缓存数据写入到网络里。在JBoss Cache 1.x.x 系列的几个版本里,这种方法逐渐被取消而采用了一种更为成熟的编码框架。在 JBoss Cache 2.x.x 系列里,这是官方支持和推荐的写入对象到数据流里的唯一机制。
Marshaller接口从 JGroups 继承了RpcDispatcher.Marshaller。这个接口有两个主要的实现- 委托的 VersionAwareMarshaller 和具体的 CacheMarshaller300。 通过调用 CacheSPI.getMarshaller() 可获得 marshaller,缺省是 VersionAwareMarshaller。用户也可以通过实现 Marshaller 接口或继承 AbstractMarshaller 类编写自己的 marshaller,并通过Configuration.setMarshallerClass() setter 将其添加到配置中。
VersionAwareMarshaller, 顾名思义,这个 marshaller 在写入时添加版本 short 到任何流里,启用相似的VersionAwareMarshaller 实例来读取版本short 并知道哪个专有的 marshaller 实现来委托调用。例如, CacheMarshaller200 是用于 JBoss Cache 2.0.x 的 marshaller。JBoss Cache 3.0.x 附带具有改进的 wire 协议的CacheMarshaller300。使用 VersionAwareMarshaller 帮助实现次要版本间的 wire 协议的兼容性,但仍然让我们可以灵活地调整和改进次要和micro 版本间的 wire 协议。
当用于应用服务器的群集状态时,部署的应用程序趋于把专有的对象实例放入到需要复制的缓存里(或者是HttpSession 对象)。由应用服务器分配独立的ClassLoader 实例到每个部署的应用程序里,但由应用服务器的 ClassLoader 来引用 JBoss Cache 库,这是很常见的。
要成功地从类加载器对对象编码和解码,我们使用一个称为区(Region)的概念。区是缓存的一部分,它共享一个公用的类加载器(区也有其他用途 - 参见接下来逐出(Eviction)的介绍)。
区是通过调用Cache.getRegion(Fqn fqn,booleancreateIfNotExists) 方法并返回Region 接口的实现来创建的。获得了区以后,区的类加载器可以设置或取消设置,而区可以激活和取消激活。在缺省情况下,区是活动的,除非 InactiveOnStartup 配置属性被设置为 true。