关于远程调用(XFire/HttpInvoker/Hessian etc.)及远程服务管理的一些随想
在现代
J2EE
企业应用系统中,存在着
Hessian
、
HttpInvoker
、
XFire
、
Axis
等多种形式的远程调用技术。尽管有
Spring
等框架对这些技术进行了封装,降低了使用的复杂度,但对普通程序员而言仍是复杂的——至少需要要掌握这些技术的基础知识。
无论使用那种技术,其基本原理都是一样的:服务端生成骨架,对外暴露服务;客户端生成服务代理,访问调用服务。通常情况下,生成服务代理的代价比较高昂,这也是我们第一次访问远程服务速度比较慢的原因,为每个请求生成新的服务代理恐怕不是我们所期望的。更何况,如果采用这种方式,就要在代码里针对各种不同的技术(如
XFire
、
HttpInvoker
)编写不同的服务生成和调用的处理代码。不仅麻烦,而且容易出错。我想,没有人愿意去直接操作各种框架技术的底层代码,这并不是一个好注意!
作为一种替代方案,我们设计了一个“服务池”的功能,或者说“服务工厂”更贴切一点。先看下面这张类图:
如上图所示,针对
HttpInvoker
、
XFire
、
Hessian
等各种远程调用技术,抽象出一个“远程服务池”(服务工厂)既
RemoteServicePool
接口。该接口提供了获取服务及一些其他的辅助功能,并针对
HttpInvoker
、
XFire
、
Hessian
等不同技术提供了相应的具体实现。采用这种方式,开发人员只需在代码中“注入”
RemoteServicePool
,并以统一的方式(如
getService()
)获取实际的服务,只是针对不同技术在配置上有些须差异而已。该技术的原理非常简单,在应用启动之前把所有存在的服务提供者提供的服务都配置好,并为它们分配一个唯一的
ID
。应用启动之后,框架会自动生成和这些地址相对应的服务代理(
ServiceProxy
),这些代理已经是可用的服务,服务获取的细节被完全屏蔽掉,开发者只要知道如何从
RemoteServicePool
中获取服务就可以了。看一下服务池的接口定义:
/**
*
远程服务缓冲池。
*
*
<p>
*
对于一个既定的服务契约(既接口),可以有很多服务提供者(
<b>
ServiceProvider
</b>
)。该接口的提出,是为了解决服务访问者和服务提供者之间
“
一对多
”
的关系。
*
*
@author
Tony
*/
public
interface
RemoteServicePool {
/**
*
从缓冲池中获取一个
<b>
Service
</b>
。
*
*
<p>
*
此处获得的是一个
<code>
Object
</code>
,需要调用者自己做类型转换。
*
*
<p>
*
参数
<b>
serviceId
</b>
代表服务缓冲池中的一个实例名称。服务类型采用配置文件中默认的类型。
*
*
@param
serviceId
*
实例名称
*
*
@return
服务对象
*/
Object getService(String serviceId);
}
|
接下来看看如何配置服务:
<bean id="userServicePool" class="com.
tonysoft
.common.XFireRemoteServicePool">
<property name="serviceInterface">
<value>com.
tonysoft
.demo.service.UserService</value>
</property>
<property name="serviceUrls">
<map>
<entry key=" server
1
">
<value>http://localhost:8080/server1/service/userService?WSDL</value>
</entry>
<entry key="server2">
<value>http://localhost:8080/server2/service/userService?WSDL</value>
</entry>
</map>
</property>
</bean>
|
最后再来看一下访问服务的代码:
/**
服务工厂
*/
public
RemoteServicePool
userServicePool
;
/**
*
测试新增一个不存在的用户。
*/
public
void
testAddUser() {
UserService
userService
= null
;
try
{
userService
=(UserService)
userServicePool
.getService("server2");
}
catch
(Exception e){
throw
new
RuntimeException(
"
获取服务失败,失败原因:"
+ e);
}
OperateResult result =
userService
.addUser(
new
User(
"daodao"
,
"
技术部"
));
assertEquals(result.isSuccess(),
true
);
}
|
该方案还为“双向关联”的系统服务提供了一个很好解决办法。看下面一张图:
如图,系统
B
和系统
C
都调用系统
A
进行付款操作;同时系统
A
要用远程服务向系统
B
或系统
C
进行认证操作,认证操作的接口(契约)都是一样的,业务逻辑可能有所差异。在这种情况下,配置在系统
A
中的认证服务就比较麻烦,因为要根据不同的系统调用认证服务,既从
B
过来的请求要访问
B
的认证服务,从
C
过来的请求要访问
C
的认证服务。用服务池可以很好的解决这个问题,把两个系统(
B
、
C
)提供的认证服务地址都配置在同一个服务池中,根据不同的
ID
(如
B
、
C
)来决定使用那个系统的服务。
尽管服务池解决了一些问题,在某种程度上降低了复杂度,但仍存在如下一些问题:
服务的运行期动态注册
服务的自动注入(
IoC
)
透明化服务
ID
的传递
在服务池(
ServicePool
)概念的基础上进行扩展,我们得出了如下的系统模型:
在核心位置上是一个服务中心资源库(
ServiceRepository
),存储了系统中用到的所有的远程服务。服务采取动态注册的机制,由对外提供的服务注册器(
ServiceRegister
)提供服务注册功能。外部系统可以实现该接口向资源中心注册服务。提供了一个启动时运行的注册器,可以把静态配置在系统中的服务都注册进来。
服务的生成、管理等均由服务中心自己维护,委托服务代理生成器(
ServiceProxyGenerator
)完成服务的创建。可以针对现有的远程调用方式,如
XFire,HttpInvoker,Hessian
等创建服务代理,也可以针对自己定义的远程调用方式创建服务代理,由
CustomServiceProxyGenerator
完成该功能。
一个服务模型包括
5
个因素:
服务接口
serviceClass
服务
ID serviceId
服务类型
serviceType
服务地址
serviceUrl
附加属性
props
查找一个服务需要两个因素,一个是服务接口,另一个是服务
ID
。这两个因素共同决定了一个服务,既服务中心内部的“服务
ID
”。通过这种方式,可以允许存在多个
ID
相同但接口不同的服务,也可以存在多个接口相同但
ID
不同的服务。
服务
ID
的获取是系统中一个关键的功能,这部分对程序员来说应该是透明的,由系统自己维护。相应的提供了一个服务
ID
提供者
(ServiceIdProvider)
接口,由实现该接口的子类完成服务
ID
获取功能(这是比较关键的地方,需要特殊考虑)。
对于程序员来说,使用服务中心里的服务再也不能比这样再简单了!看看配置:
<
bean
id
=
"helloHttpInvokerService"
parent
=
"abstractServiceProxyFactory"
>
<
property
name
=
"serviceInterface"
>
<
value
>
com.tonysoft.common.service.repository.example.HelloHttpInvoker
</
value
>
</
property
>
</
bean
>
|
再看如何使用这个
bean
:
private
HelloHttpInvoker
helloHttpInvokerService
;
public
void
testHttpInvoker() {
assertNotNull(
"helloHttpInvokerService can't be null !"
,
helloHttpInvokerService
);
assertEquals
(
"Hello , HttpInvoker !"
,
helloHttpInvokerService
.sayHello());
}
/**
*
@param
helloHttpInvokerService
*
the
helloHttpInvokerService
to
set
*/
public
void
setHelloHttpInvokerService(HelloHttpInvoker helloHttpInvokerService) {
this
.
helloHttpInvokerService
= helloHttpInvokerService;
}
|
就是这样的简单!
Spring
会把这个
bean
自动注入到程序中,可以象使用其他任何
bean
一样使用它!程序员完全不用关心该服务由谁提供、采用什么技术,他只要知道系统中存在这样一个服务就
OK
了。该技术彻底向程序员屏蔽了底层技术的实现细节,以统一的方式访问任何形式的远程服务。至于服务是如何生成、如何配置的将在后面叙述。
服务(
Service Bean
)是如何实现自动注入(
IoC
)的呢?
注意到上面配置的
bean
都继承了“
abstractServiceProxyFactory
”,它是一个工厂
bean
,负责根据给定的接口类型,到服务中心(
ServiceRepository
)查找服务,并生成服务代理。我们来看一下它的核心代码:
/**
*
服务代理工厂。
*
*
<p>
*
该工厂对程序员屏蔽了服务实现的技术细节,对于
XFire
、
Hessian
、
HttpInvoker
等常用远程服务形式进行封装。
*
*
<p>
*
程序员只需要提供一个服务接口(契约),该工厂会从服务中心
<code>
ServiceRepository
</code>
中查找符合该接口的远程服务实例。
*
*
<p>
*
查找的规则是由服务
ID
提供者所提供的服务
ID
和服务接口名字共同组成的服务关键字匹配。
*
*
@author
Tony
*/
public
class
ServiceProxyFactory
implements
FactoryBean {
/**
服务中心
*/
private
ServiceRepository
serviceRepository
;
/**
服务
ID
提供者
*/
private
ServiceIdProvider
serviceIdProvider
;
/**
服务接口
*/
private
Class
serviceInterface
;
/*
* @see org.springframework.beans.factory.FactoryBean#getObject()
*/
public
Object getObject()
throws
Exception {
return
ProxyFactory.getProxy(getObjectType(),
new
ServiceProxyInterceptor());
// return serviceRepository.getService(serviceInterface, serviceIdProvider.getCurrentServiceId());
}
/*
* @see org.springframework.beans.factory.FactoryBean#getObjectType()
*/
public
Class getObjectType() {
return
serviceInterface
;
}
/*
* @see org.springframework.beans.factory.FactoryBean#isSingleton()
*/
public
boolean
isSingleton() {
return
true
;
}
/*
*
远程服务代理拦截器。
*/
private
class
ServiceProxyInterceptor
implements
MethodInterceptor {
/*
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
public
Object invoke(MethodInvocation invocation)
throws
Throwable {
Method method = invocation.getMethod();
Object[] args = invocation.getArguments();
Object client = getClient();
return
method.invoke(client, args);
}
private
Object getClient() {
try
{
return
serviceRepository
.getService(
serviceInterface
,
serviceIdProvider
.getCurrentServiceId());
}
catch
(ServiceException e) {
//
TODO
e.printStackTrace();
return
null
;
}
}
}
// ----
容器自动注入
----
······
|
我们注意上面加粗、倾斜的那段代码:
return
serviceRepository
.getService(
serviceInterface
,
serviceIdProvider
.getCurrentServiceId());
|
真正的魅力就在这个地方。根据服务接口类型和服务
ID
,从服务中心获取特定的服务。服务接口是配置好的,
而服务
ID
则在运行时才能确定,根据不同的应用、不同的策略提供不同的
ServiceIdProvider
。其中用到了
Spring
的
FactoryBean
和拦截器,至于为什么要在这里使用拦截器,可以参考
Spring
框架的源码。
服务代理生成器(
ServiceProxyGenerator
)也是一个值得一提的地方,我们先看一下它的接口:
/**
*
服务代理生成器。
*
*
@author
Tony
*/
public
interface
ServiceProxyGenerator {
/**
*
取得服务代理对象。
*
*
@param
serviceClass
*
服务接口
*
@param
serviceUrl
*
服务地址
*
@param
props
*
附加属性
*
@return
代理对象
*
@throws
Exception
*/
Object getService(Class serviceClass, String serviceUrl, Properties props)
throws
Exception;
}
|
它只有一个
getService()
方法,那么为什么设计这个接口?在什么地方使用呢?回答这个问题之前先来看看下面这段代码:
public
void
registService(ServiceModel serviceModel)
throws
ServiceException {
······
String key = serviceModel.getServiceId() +
KEY_SPAR
+ serviceModel.getServiceClass().getName();
if
(
serviceNames
.contains(key)) {
throw
new
ServiceRegistException(
"service is exist!"
);
}
Object proxy =
null
;
try
{
ServiceProxyGenerator proxyGenerator = (ServiceProxyGenerator)
beanFactory
.getBean(serviceModel.getServiceType() +
PROXY_GENERATOR_END
);
proxy = proxyGenerator.getService(serviceModel.getServiceClass(), serviceModel
.getServiceUrl(), serviceModel.getProps());
}
catch
(Exception e) {
throw
new
ServiceRegistException(
"can't regist service !"
, e);
}
if
(proxy !=
null
) {
serviceNames
.add(key);
serviceContainer
.put(key, proxy);
}
else
{
throw
new
ServiceRegistException(
"fail to regist service !"
);
}
}
|
上面做特殊标记的代码就是应用服务代理生成器的地方,这里我们用到了
Spring
的
bean
工厂,根据注册服务的类型(
xfire,httpinvoker,hessian
等)到
Spring
容器里查找相应的生成器,并生成指定类型的服务。看下面配置的几个服务代理生成器:
<!-- XFire
类型服务代理生成器
-->
<
bean
id
=
"xfire_generator"
class
=
"com.tonysoft.common.service.repository.generator.XFireServiceProxyGenerator"
lazy-init
=
"true"
>
<
property
name
=
"serviceFactory"
>
<
ref
bean
=
"xfire.serviceFactory"
/>
</
property
>
</
bean
>
<!-- Hessian
类型服务代理生成器
-->
<
bean
id
=
"hessian_generator"
class
=
"com.tonysoft.common.service.repository.generator.HessianServiceProxyGenerator"
lazy-init
=
"true"
>
</
bean
>
<!-- HttpInvoker
类型服务代理生成器
-->
<
bean
id
=
"httpinvoker_generator"
class
=
"com.tonysoft.common.service.repository.generator.HttpInvokeServiceProxyGenerator"
lazy-init
=
"true"
>
</
bean
>
<!--
自定义
类型服务代理生成器
-->
<
bean
id
=
"custom_generator"
class
=
"com.tonysoft.common.service.repository.generator.CustomServiceProxyGenerator"
lazy-init
=
"true"
>
</
bean
>
<!--
服务中心(资源库)
-->
<
bean
id
=
"serviceRepository"
class
=
"com.tonysoft.common.service.repository.DefaultServiceRepository"
>
</
bean
>
<!--
服务
ID
提供者
-->
<
bean
id
=
"serviceIdProvider"
class
=
"com.tonysoft.common.service.repository.provider.DefaultServiceIdProvider"
>
</
bean
>
<!--
所有远程服务的基础类
-->
<
bean
id
=
"abstractServiceProxyFactory"
class
=
"com.tonysoft.common.service.repository.ServiceProxyFactory"
abstract
=
"true"
>
</
bean
>
|
简单看一下
HttpInvoker
类型服务代理生成器的代码:
public
class
HttpInvokeServiceProxyGenerator
implements
ServiceProxyGenerator {
/**
HttpInvoker
服务代理工厂
*/
private
HttpInvokerProxyFactoryBean
httpInvokerFactory
=
new
HttpInvokerProxyFactoryBean();
/*
* @see com.alipay.xfiredemo.common.ServiceProxyGenerator#getService(java.lang.Class, java.lang.String,
* java.util.Properties)
*/
public
Object getService(Class serviceClass, String serviceUrl, Properties props) {
// Todo initial httpInvokerFactory with props
httpInvokerFactory
.setServiceInterface(serviceClass);
httpInvokerFactory
.setServiceUrl(serviceUrl);
// must invoke this method
httpInvokerFactory
.afterPropertiesSet();
return
httpInvokerFactory
.getObject();
}
}
|
是的,正如你所看到的一样,我们这里把真正生成服务代理的任务交给了
Spring
的
HttpInvokerProxyFactoryBean
来完成。
提供在初始化时注册的静态服务功能,配制如下:
<!--
初始化时注册的静态服务
-->
<
bean
id
=
"bootupServiceRegister"
class
=
"com.tonysoft.common.service.repository.register.BootupServiceRegister"
lazy-init
=
"false"
>
<
property
name
=
"services"
>
<
list
>
<!--
<bean class="com.tonysoft.common.service.repository.ServiceModel">
<property name="serviceClass"><value>com.tonysoft.common.service.repository.example.HelloHessian</value></property>
<property name="serviceId"><value>default</value></property>
<property name="serviceType"><value>hessian</value></property>
<property name="serviceUrl"><value>http://localhost:8080/serviceRepositoryApplication/service/hessian/helloHessian.service</value></property>
<property name="props">
<props></props>
</property>
</bean>
-->
<
bean
class
=
"com.tonysoft.common.service.repository.ServiceModel"
>
<
property
name
=
"serviceClass"
><
value
>
com.tonysoft.common.service.repository.example.HelloHttpInvoker
</
value
></
property
>
<
property
name
=
"serviceId"
><
value
>
default
</
value
></
property
>
<
property
name
=
"serviceType"
><
value
>
httpinvoker
</
value
></
property
>
<
property
name
=
"serviceUrl"
><
value
>
http://localhost:8080/serviceRepositoryApplication/service/httpInvoker/helloHttpInvoker.service
</
value
></
property
>
<
property
name
=
"props"
>
<
props
></
props
>
</
property
>
</
bean
>
<
bean
class
=
"com.tonysoft.common.service.repository.ServiceModel"
>
<
property
name
=
"serviceClass"
><
value
>
com.tonysoft.common.service.repository.example.HelloXFire
</
value
></
property
>
<
property
name
=
"serviceId"
><
value
>
default
</
value
></
property
>
<
property
name
=
"serviceType"
><
value
>
xfire
</
value
></
property
>
<
property
name
=
"serviceUrl"
><
value
>
http://localhost:8080/serviceRepositoryApplication/service/xfire/helloXFire.service?WSDL
</
value
></
property
>
<
property
name
=
"props"
>
<
props
></
props
>
</
property
>
</
bean
>
</
list
>
</
property
>
</
bean
>
|
具体内容可以参看附件中的资源:
一、
ServiceRepository
的源代码(
Eclipse
工程)
二、
一个示例应用
三、
打包部署的
ANT
脚本
把项目导入
Eclipse
中,直接运行
Ant
脚本,在
target
目录下会生成服务中心的
jar
包,同时生成示例应用的
war
包,把
war
包放到任意服务器(
Server
)上并启动服务器并确保应用正常启动。
运行
ServiceRepositoryTest
.java
执行完整的单元测试,观测结果。其他的自己看源码吧。
下载附件