15.2.2 装配 RMI 服务

传统上,RMI 客户端必须使用 RMI API 的 Naming 类从 RMI 注册表中查找服务。例如,下面的代码片段演示了如何获取 Spitter 的 RMI 服务:

try {
  String serviceUrl = "rmi:/spitter/SpitterService";
  SpitterService spitterService = (SpitterService) Naming.lookup(serviceId)
  ...
} catch (RemoteException e) {
  ...
} catch (NotBoundException e) {
  ...
} catch (MalformedURLException e) {
  ...
}

虽然这段代码可以获取 Spitter 的 RMI 服务的引用,但是它存在两个问题:

  • 传统的 RMI 查找可能会导致 3 种检查型异常的任意一种 (RemoteException、NotBoundException 和 MalformedURLException),这些异常必须被捕获或重新抛出;

  • 需要 Spitter 服务的任何代码都必须自己负责获取该服务。这属于样板代码,与客户端的功能并没有直接关系。

RMI 查找过程中所抛出的异常通常意味着应用发生了致命的不可恢复的问题。例如,MalformedURLException 异常意味着这个服务的地址是无效的。为了从这个异常中恢复,应用至少要重新配置,也可能需要重新编译。try/catch 代码块并不能在发生异常时优雅地恢复,既然如此,为什么还要强制我们的代码捕获并处理这个异常呢?

但是,更糟糕的事情是这段代码直接违反了依赖注入(DI)原则。因为客户端代码需要负责查找 Spitter 服务,并且这个服务是 RMI 服务,我们甚至没有任何机会去提供 SpitterService 对象的不同实现。理想情况下,应该可以为任意一个 bean 注入 SpitterService 对象,而不是让 bean 自己去查找服务。利用 DI,SpitterService 的任何客户端都不需要关心此服务来源于何处。

Spring 的 RmiProxyFactoryBean 是一个工厂 bean,该 bean 可以为 RMI 服务创建代理。使用 RmiProxyFactoryBean 引用 SpitterService 的 RMI 服务是非常简单的,只需要在客户端的 Spring 配置中增加如下的 @Bean 方法:

@Bean
public RmiProxyFactoryBean spitterService() {
  RmiProxyFactoryBean rmiProxy = new RmiProxyFactoryBean();
  rmiProxy.setServiceUrl("rmi://localhost/SpitterService");
  rmiProxy.setServiceInterface(SpitterService.class);
  return rmiProxy;
}

服务的 URL 是通过 RmiProxyFactoryBean 的 serviceUrl 属性来设置的,在这里,服务名被设置为 SpitterService,并且声明服务是在本地机器上的;同时,服务提供的接口由 serviceInterface 属性来指定。图 15.5 展示了客户端和 RMI 代理的交互。

现在已经把 RMI 服务声明为 Spring 管理的 bean,我们就可以把它作为依赖装配进另一个 bean 中,就像任意非远程的 bean 那样。例如,假设客户端需要使用 Spitter 服务为指定的用户获取 Spittle 列表,我们可以使用 @Autowired 注解把服务代理装配进客户端中:

@Autowired
SpitterService spitterService;

我们还可以像本地 bean 一样调用它的方法:

public List<Spittle> getSpittles(String userName) {
  Spitter spitter = spitterService.getSpitter(userName);
  return spitterService.getSpittersForSpitter(spitter);
}

以这种方式访问 RMI 服务简直太棒了!客户端代码甚至不需要知道所处理的是一个 RMI 服务。它只是通过注入机制接受了一个 SpitterService 对象,根本不必关心它来自何处。实际上,谁知道客户端得到的就是一个基于 RMI 的实现呢?

此外,代理捕获了这个服务所有可能抛出的 RemoteException 异常,并把它包装为运行期异常重新抛出,这样我们就可以放心地忽略这些异常。我们也可以非常容易地把远程服务 bean 替换为该服务的其他实现 —— 或许是不同的远程服务,或者可能是客户端代码单元测试时的一个 mock 实现。

虽然客户端代码根本不需要关心所赋予的 SpitterService 是一个远程服务,但我们需要非常谨慎地设计远程服务的接口。提醒一下,客户端不得不调用两次服务:一次是根据用户名查找 Spitter,另一次是获取 Spittle 对象的列表。这两次远程调用都会受网络延迟的影响,进而可能会影响到客户端的性能。清楚了客户端是如何使用服务的,我们或许会重写接口,把这两个调用放进一个方法中。但是现在我们要接受这样的服务接口。

RMI 是一种实现远程服务交互的好办法,但是它存在某些限制。首先,RMI 很难穿越防火墙,这是因为 RMI 使用任意端口来交互 —— 这是防火墙通常所不允许的。在企业内部网络环境中,我们通常不需要担心这个问题。但是如果在互联网上运行,我们用 RMI 可能会遇到麻烦。即使 RMI 提供了对 HTTP 的通道的支持(通常防火墙都允许),但是建立这个通道也不是件容易的事。

另外一件需要考虑的事情是 RMI 是基于 Java 的。这意味着客户端和服务端必须都是用 Java 开发的。因为 RMI 使用了 Java 的序列化机制,所以通过网络传输的对象类型必须要保证在调用两端的 Java 运行时中是完全相同的版本。对我们的应用而言,这可能是个问题,也可能不是问题。但是选择 RMI 做远程服务时,必须要牢记这一点。

Caucho Technology(Resin 应用服务器背后的公司)开发了一套应对 RMI 限制的远程调用解决方案。实际上,Caucho 提供了两种解决方案:Hessian 和 Burlap。让我们看一下如何在 Spring 中使用 Hessian 和 Burlap 处理远程服务。

Last updated