第33章 Spring远程方案(一)

第33章 Spring远程方案

本章内容

  • 从“对面交谈”到“千里传声”

  • Spring Remoting架构分析

  • Spring Remoting提供的远程服务支持

  • 扩展Spring Remoting

  • Spring Remoting之扩展篇

33.1 从“对面交谈”到“千里传声”

话说某日,八神突发要跟草稚对决之念头,便直接走上街头去堵草稚。在二人面对面站定之后,八神便直接向草稚发话,要求决斗云云,草稚当然可以接受其挑战,也可以拒绝之。整个场景用图说话可能更具说服力一些,如图33-1所示。

image-20220722112010855

不要误会,我不是在讨论《拳皇》,实际上是要讨论对象间的调用。确切地说,是本地调用。你难道不觉得图33-1所展示的场景跟对象间的本地调用很相似吗?因为八神对象需要请求草稚对象做什么事情的时候可以直接找到对方(在本地调用中就相当于能够直接获得对方的引用),所以直接发送“对决”调用请求即可。至于调用的结果,那完全要看被调用的草稚对象如何处理调用请求了。如果他答应八神对象的“对决”调用请求,那相当于调用正常返回;相反,如果回绝,那跟我们的业务对象通过抛出业务相关异常来表示调用失败又是多么地相似。

大部分时间,我们都是在开发这种由本地调用组成的软件系统,甚至于在继续余下内容之前,Spring框架为我们所提供的各种帮助也大多是局限于此。不过,并不是什么事情都会按照我们的意愿行事,当八神再次想与草稚对决的时候,情况发生变化了…八神当然希望能够面对面地跟草稚挑明,但不巧的是,草稚现在不在当前进程中,即不在本地,他现在身在异乡(如图33-2所示)。

image-20220722112139595

这种情况下,八神该怎么跟草稚进行交流呢?不嫌慢并且草稚那边有固定地址,那写信吧!如果等不及,并且草稚现在没有固定地址,那有电话也行啊!什么?电话也没有,那能上网不?MSN或者Skype都行,甚至于不管什么手段,只要能跟草稚对象联系上,把对决的请求发送给他然后获得相应答复就行啊!

你看,就因为两个对象相隔太远,只要它们需要交流,我们就不得不想尽各种办法把它们联系到一起。现实世界中,我们可以通过电话,IM(Instant Messaging)软件等。当把草稚和八神化作两个位于不同进程,或者不同主机上运行的对象的时候,我们就得使用其他方式,比如RMI、EJB、Web服务等手段,现在,我们进入了分布式应用的地盘。

像草稚和八神这种,因为地域界限(映射到远程访问就是进程间或者主机间的界限)而不得不采用各种远程方式进行相互访问的情况,我们可以将其看作是“被逼无奈”之举。如果位于中国的某个应用需要访问位于美国某台主机上的某种服务,那么它就不得不采用某种远程方式来完成服务的访问;但对于某些应用来说,引入某些远程机制从而实现分布式架构,却是它们本身“自愿”的,因为它们可以从分布式架构中获取它们需要的东西,比如,通过负载均衡以扩展系统性能,通过多台并行主机让系统获得“灾难恢复”的能力,通过统一部署简化系统维护,等等。当然,我们可以列举更多,但是,任何东西的获取都是有“机会成本”存在的。分布式架构带给我们的好处也同样需要付出相应的代价,最为突出的问题就是整个系统的复杂度突增。相对于本地调用来说,我们现在要饱受网络延迟之苦,要考虑并处理多个请求对同一对象(或者组件)的并发访问问题。如果系统出现故障,我们大多无法处理,因为既可能是网络问题,也可能是被访问端出现故障。总之,现在我们要考虑太多太多,这或许也正是为什么MartinFowler提出的分布式对象第一原则是“不要分布你的对象”的原因吧!

那是不是因为分布式架构的复杂度,我们就真得不去分布我们的对象了呢?这应该由应用场景来决定。既然分布式架构利弊都有,我们就需要权衡。当我们的应用能够从分布式场景中获得更多的“利”的时候,采用分布式架构是合理的,而只能获得少量的"利”,甚至"微利”的时候,采用分布式架构反而是不合适的。

对Java开发人员来说,一提到分布式架构,第一个跃入脑海的多半就是EJB(我指的是EJB1.x~2.x)。应该说,在处理分布式需求方面,EJB表现还是不错的,部分功能方面的设计失败,并不能掩盖EJB在分布式处理方面的不错表现,但是,为什么现在EJB受人诟病那么多呢?EJB中部分功能设计,比如实体Bean(Entity Bean),确实为败笔,但更多时候,EJB的诟病却是由其提供分布式服务的本质而招致的。分布式架构本身就会为整个应用引入过多的复杂度,所以,对分布式功能提供支持的EJB自然也就简单不到哪儿去。在我们倡导Without EJB的开发模式的时候,更多是因为我们所处的应用场景并不需要引入分布式的概念,Spring提倡的轻量级开发方案不是彻底地否定EJB,而是否定对EJB的滥用,或者更广一些,对各种技术方案在不恰当场景中的滥用。

如果按照“帕累托法则”(Pareto Principle,也叫80/20法则)进行划分的话,我们日常所开发的应用大都属于不需要分布式架构的那80%之列,那么,正常情况下,我们就不应该引入分布式的概念,但是对EJB的滥用却使得这80%的净土也让分布式的复杂度所污染。这就好像你本来不需要的东西,别人一定要硬塞给你一样,到时候沉的拿不动还是小事,还要跟你要银子的话,那岂不更惨?难道当年那些不需要EJB提供分布式支持的应用,被硬是扣上EJB这顶沉重的帽子,与此不是很相似吗?鉴于此,Spring才提出Without EJB的理念,Spring的开发理念希望我们轻装上阵,在系统设计和开发之初,只需要关注核心业务之类关注点,对于像分布式之类的需求暂且扔到一边,待需要用的时候再添加也不迟。况且,只要设计良好,为现有系统添加分布式访问能力也并不像我们想象的那么棘手,尤其是在有了Spring Remoting的相关框架类支持之后,完成从本地调用到远程调用的转变变得更是得心应手,所以,还是赶快开始我们的Spring Remoting之旅吧!

33.2 Spring Remoting架构分析

为了帮助我们简化公开和访问远程服务的相关工作,Spring Remoting推出了“Service Exporter和Service Accessor组合”。通过该魅力组合,我们可以统一的风格将本地服务以远程的形式公开出去,也可以统一的风格来访问远程服务。在Spring Remoting为我们准备好了针对不同远程机制的“Service Exporter和Service Accessor组合”之后,我们所要做的,也就剩下简单的配置工作了。

除了核心的“Service Exporter和Service Accessor组合”,SpringRemoting同时也考虑到了远程访问过程中可能发生的各种访问异常,并给出了一套标准远程访问异常体系。在我们详细了解核心的“Service Exporter和Service Accessor组合”之前,不妨先把这套远程访问异常体系简单看一下吧!

33.2.1 Spring Remoting之远程访问异常体系

你或许会疑惑,远程访问异常是哪一类异常啊?对于本地调用来说,我们从来就不会操心这些,因为它在那种场景下并不存在,远程访问异常纯粹是引入分布式架构,需要通过各种远程机制进行服务访问后的产物。当八神对象通过电话这种远程机制调用草稚对象服务方法的时候,有可能电话线路出现故障,也有可能草稚的手机没电或者关机,这不同于本地调用时候草稚对象抛出的业务相关异常。对于远程访问期间出现的这类异常情况,调用端通常无法有效处理。我想电话线路故障,即使八神对着电话一个劲地吼,电话线路也不太可能自动恢复正常工作吧?对于其他远程机制,也是同样的道理。

鉴于我们通常无法有效处理远程访问异常的事实,Spring Remoting框架提出了一套unchecked exception类型的远程访问异常体系,如图33-3所示。

image-20220722112838595

对于这些异常,远程服务调用方既可以任其在调用栈中向上传播,由外层决定如何对其进行处理,也可以自己捕捉以决定如何处置。当然,最主要的,现有的各种远程方案都有自己特定的远程访问异常类型,采用Spring Remoting的这套标准远程访问异常体系之后,各种远程方案特定的异常类型将被转译。这样,远程方案的变动,起码在异常处理这一层次上,不会对调用方造成太大的影响。

33.2.2 统一风格的远程服务公开与访问方式

我们可以在分布式系统中发现多种远程交互方式,比如面向消息中间件(MOM)使用的MessagePassing方式,或者数据库管理系统(DBMS)所使用的共享存储区(Shared Repository)方式,但最基本也最广为人知的却应该是RPC(Remote Procedure Call)方式。而Spring Remoting提供的远程服务公开以及访问方式,基本上就是建立在RPC的行为基础上的。

RPC远程交互方式建立在客户端/服务器(Client-Server)模型上。在客户端/服务器模型中,由客户端发起调用请求,请求数据(比如请求的函数名以及参数等)通过相应的协议传输到服务器端(Server)再交给被请求的服务进行处理,最后的处理结果再传送给请求发起的客户端。在处理结果返回之前,客户端通常是阻塞(Blocking)状态。对于一个设计良好的系统来说,不太可能会将远程交互相关的细节全部都附加给业务对象(服务器端的也好,客户端的也好)。如果那样的话,业务对象将陷入远程交互细节之类的泥潭,而无暇顾及真正该其关心的业务逻辑实现。所以,将服务器端以及客户端业务对象通过远程机制进行交互这一关注点,单独剥离到相应的角色来处理,就成为很自然的一件事情。Spring Remoting所做的工作实际上就是如此。

正如我们开始所提到的那样,Spring Remoting最核心的就是它所推出的“Service Exporter和Service Accessor组合”(见图33-4)。

在这一组合中,Service Exporter将主要负责远程服务对象的公开工作,具体点儿说就是,负责根据使用的远程机制(RMI也好,EJB也好,Hessian也好,等等)接收服务请求,然后对请求内容进行解组(un-marshaling)。根据解组后的请求内容调用本地服务对象(对于Client是远程对象的服务对象,对于Exporter来说,就是本地对象了)。调用完成后,将调用结果重新编组(marshaling)然后发送到请求客户端。与此相反,Service Accessor的主要职责当然就是帮助客户端对远程服务对象进行远程访问。在客户端发起调用请求之后,相应的Service Accessor需要对请求内容进行编组,然后根据所使用的远程机制(RMI、Web服务等)对编组后的请求内容进行发送。在接收到Service Exporter发送回的调用结果之后,再对调用结果进行解组,并把解组后的调用结果传给本地的客户端对象使用。

image-20220722113201010

在引入“Service Exporter和Service Accessor组合”之后,无论是客户端对象还是远程服务对象都落得个“无远程责任一身轻”,它们只需要关注自己应该关注的业务逻辑即可,对于远程交互之类的事情,就全部扔给具体的Service Exporter和Service Accessor实现类去处理了。这就跟八神和草稚通过电话这种远程机制进行交互一样,至于电话如何将语音信号编组为模拟信号或者数字信号,八神和草稚根本就不需要去关心,他们所要作,只是发话和回话而已。这恰好也正是为什么图33-4与图33-2“对决之“远程调用”版”看起来如此相似,Service Exporter和Service Accessor不就相当于生活场景中的被叫方和主叫方的电话或者MSN等IM客户端吗?

实际上,Spring提供的“Service Exporter和Service Accessor组合”完全就是软件架构中处理远程调用的基本模式的一个缩影。从Spring Remoting对各种远程机制的支持推广开来,我们可以得出一个“几乎”是“放之Remoting世界而皆准”的结论,任何的软件系统,如果它要将本地的服务以远程的形式公开给他人使用,通常都会设置一个类似Service Exporter的角色来负责远程交互的细节。在现有的Remoting模式中,Service Exporter的角色通常称为Invoker;相对地,当本地对象或者组件需要依赖于以远程的形式公开的某一服务的时候,系统中也通常会设置一个类似于Service Accessor的角色负贵处理与远程服务的交互工作。在现有的Remoting模式中,Service Accessor的角色通常称为Requestor。不过,Spring Remoting的Service Accessor要比Requestor更进一步。更确切地讲,应该属于ClientProxy,因为它们几乎都是清一色的ProxyFactoryBean,可以为本地调用方提供远程服务的代理对象,从而使得本地调用方只需要跟远程服务接口打交道,完全不需要像“过程化编程”时代的RPC那样,需要直接跟Requestor打交道,这使得本地调用或者远程调用的事实对本地调用方来说是完全透明的。

说这么多,唯一的目的就是,当Spring Remoting所提供的对各种远程机制的支持不能满足我们需要的时候,我们完全可以“依葫芦画瓢”,以几乎相同的Remoting模式来实现自己的远程方案。不过,Spring Remoting为现在各种远程机制所提供的支持已经足够丰富了。所以,在考虑自己提供基于某种远程机制的远程方案之前,我们应该首先对Spring Remoting所提供的各种方案给予足够的重视,否则,重新赶工所浪费的人力物力就看起来有些可惜了!

注意:Spring Remoting提供的远程方案通常都是基于无状态的远程服务,这使得整个系统的演化可以更加灵活。如果当前场景“确实”需要有状态的远程交互,那么可以考虑EJB的有状态的Session Bean或者其他方案。但前提一定要明确,只有真正必要的时候才这么做。

33.3 Spring Remoting提供的远程服务支持

在开始了解Spring Remoting提供的各种远程方案支持之前,我们需要有一个可以讨论的基石,你还记得Spring MVC部分中提到的ITMRateService吗?当时我们是通过它提供获取TTM汇率的服务,然后通过web应用的形式将请求结果公开给客户端浏览器,而现在,我们将使用各种远程方案把同一个ITTMRateService公开给不同的客户端使用,如图33-5所示。

image-20220722145221978

你将看到,通过Spring Remoting以远程方式公开一个本地服务是多么简单,而客户端应用访问这一远程服务又是多么的省事。

虽然我们当前场景中使用的是单一业务对象的Remoting,但是这完全可以扩展到整个应用的场景中。只要应用程序的业务层设计良好,我们后期完全可以根据不同的客户端类型需求,使用对应的远程机制将业务层提供的服务以远程方式公开出去。整个应用的灵活性和可扩展性可见一斑!从这个角度来讲,我们更应该坚持以业务逻辑为中心的开发准则。无论什么情况下,系统中的业务逻辑都应该以较为独立的形式存在,这些业务逻辑只应该关注自已所关注的东西,依赖自己最需要依赖的对象。而至于谁将使用它们,将以什么样的形式使用它们,不应该是业务逻辑实现所需要关心的事情。我们要对业务对象进行任务调度也好,对业务对象通过远程公开使用也好,业务对象实现期间根本就不需要关心。当这些需求涌现的时候,可以在业务对象的基础上为它们提供相应的Wrapper或者Facade。让这些Wrapper或者Facade帮助当前业务对象独当一面,只有真正需要业务逻辑的时候,再将实质性的请求委派给业务对象处理。

我想,Spring Remoting带给我们的不只是一套现成的框架类以供我们使用。从一个侧面上来看,它向我们传达的是一种理念,以业务逻辑为中心进行开发的理念,坚持这种理念,整个系统的演化将变得不再举步维艰。

33.3.1 基于RMI的Remoting方案

RMI是JavaSE平台标准的远程机制,它采用Java的序列化(Java Serialization)机制进行远程数据传输。Spring Remoting对现有的RMI使用进行了封装,在支持传统的RMI编程的基础上,还引入了RMI Invoker机制来进一步解除业务对象与RMI使用之间的耦合。

1. 通过RMI公开远程服务

按照RMI的要求,要想将某个服务通过RMI公开出去,它的接口定义必须符合某些要求。比如,接口定义需要继承java.rmi.Remote,接口中需要公开的操作对应的方法定义需要抛出RemoteException异常。所以,要想将我们的ITMRateService以RMI公开出去,看来得对其做点儿手脚了,如下所示:

public interface ITIMRateService extends Remotel {
    
    
	List<TTMRate> getTTMRatesByTradeDate(TradeDate tradeDate) throws RemoteException;
}

另外,业务接口中定义的方法参数和返回值都必须符合Java序列化的要求。因为我们的TradeDate使用了JodaTime的DateTimeFormatter,该类不符合要求,所以,我们暂且将其定义为transient,以避免序列化问题,如下所示:

private final transient DateTimeFormatter defaultDateTimeFormatter = DateTimeFormat.forPattern("yyYyMdd");

在业务接口和实现类都符合RMI的要求之后,我们就可以通过Spring Remoting的org.springframework.remoting.rmi.RmiServiceExporter将其公开出去了,如下方代码清单所示。

# server-config,xm1
<bean id="ttmService" class="..MockTTMRateService">
</bean>

<bean id="ttmServiceWithRMIRemoting" class="org.springframework.remoting.rmi.Rmi.ServiceExporter">
	<!-- with serviceName, we can access it with URL like [rmi://host:port/serviceName] -->
	<property name="serviceName" value="TtmService"/>
	<property name="serviceInterface" value="..ITTMRateService"/>
	<property name="service" ref="ttmService"/>
	<property name="registryPort" value="1099"/>
</bean>

现在,RMI客户端可以从rmi://yourhost:1099/TtmService对我们的ITTMRateService服务进行访问。

应该说,RMI需要相应的业务接口去实现Remote接口,以及抛出RemoteException等要求并不过分。毕竞,要在茫茫“类”海中寻找需要RMI服务的类终究需要某些标志吧?但是,将这些要求直接加诸到我们的业务接口定义以及实现类上,那就是我们的不对了!我们已经说了,业务对象只需要关心业务逻辑相关的关注点,对于使用它的方式,它应该一概不管,可现在我们显然是逆之而行了。实际上,我们完全可以把RMI需要业务接口戴上的那顶帽子转扣在别人头上,找个经纪人而已嘛,当真正需要的时候再来找我们的业务对象也不迟啊!

Spring Remoting引入了RMI Invoker的机制,来避免Remote接口以及RemoteException对业务接口和相应实现类的侵入,所以,如下这样保持我们最初的ITTMRateService定义不变:

public interface ITTMRateService {
    
    
	List<TTMRate> getTTMRatesByTradeDate(TradeDate tradeDate);
}

使用同样的RmiServiceExporter,同样的配置,我们依然可以将ITTMRateService以RMI的形式公开给相应客户端使用。只不过,RmiServiceExporter现在使用的是Spring提供的RMI Invoker机制,而不是最初的RMI经典方式。

对于RMI Invoker的运作机制来说,总的原则就是把原来扣在ITTMRateService头上的RMI要求的那顶帽子,转扣在别人的头上。在Spring Remoting中,就是RemoteInvocationHandler,其定义如下:

public interface RmiInvocationHandler extends Remote {
    
    
	publicStringgetTargetInterfaceName()throwsRemoteException;
	public Object invoke(RemoteInvocation invocation) throws RemoteException, NoSuchMethodException, Il1ega1AccessException, InvocationTargetException;
}

可以看到,Remote接口和RemoteException现在全部扣在了RmiInvocationHandler的头上。当RmiServiceExporter检测到业务接口没有符合RMI要求的时候,它就会使用RMI Invoker机制,从invoke(RemoteInvocation)方法的参数中获取被调用的业务方法信息,然后通过反射调用目的业务对象的方法。这也就是业务接口得以从RMI的束缚中解脱的全部奥秘了。

因为RMI Invoker使用的是特定的传输对象(RemoteInvocation),所以对RMI Invoker公开的业务对象,只能通过Spring Remoting提供的RmiProxyFactoryBean进行访问。而对于按照RMl要求公开的服务接口,通过RmiServiceExporter公开之后,除了可以使用RmiProxyFactoryBean进行访问之外,普通的RMI客户端也同样可以访问。

2. 通过RMI访问远程服务

假设我们有依赖ITTMRateService的客户端对象定义如下:

public class TTMServiceClient {
    
    
	private ITTMRateService ttmService;
  
	public void doSth() {
    
    
		List<TTMRate>ttmRates = getTtmService().getTTMRatesByTradeDate(TradeDate.valueOf("20080302"));
    	...
  	}
 	// getter和setter方法定义·····
}

如果像当初那样,TMServiceClient与ITTMRateService实现类同处一地,我们可以直接将ITTMRateService实现类注入给它。但是,现在它们身居两地,我们就得另寻他法了。好在ttmService已经通过RMI以远程服务的形式公开了出来,我们可以使用org.springframework.remoting.rmi.RmiProxyFactoryBean来访问它。在RmiProxyFactoryBean创建了一个ITTMRateService的代理对象之后,我们就可以鱼目混珠,以ITTMRateService的代理对象代替实质的ITTMRateService实现类注入给TTMServiceClient使用,如下代码所示:

# client-config,xm1
<bean id="ttmService" class="org.springframework,remoting.rmi.Rmi,ProxyFactoryBean">
	<property name="serviceUrl" value="rmi://yourhost:1099/TtmService"/>
	<property name="serviceInterface" value="..ITTMRateService"/>
</bean>

<bean id="client"class="..TTMServiceClient">
	<property name="ttmService" ref="ttmService"/>
</bean>

RmiProxyFactoryBean使得TTMServiceClient对ITTMRateService的依赖变得透明。虽然现在的ITTMRateService是以远程调用方式进行访问的,可是TTMServiceClient却是浑然不知,或许依然以为是在以本地调用的方式使用ITTMRateService呢!

猜你喜欢

转载自blog.csdn.net/qq_34626094/article/details/125957282