Spring为各种远程访问技术的集成提供了工具类。Spring远程支持是由普通(Spring)POJO实现的,这使得开发具有远程访问功能的服务变得相当容易。目前,Spring支持四种远程技术:
远程方法调用(RMI)。通过使用 RmiProxyFactoryBean 和 RmiServiceExporter,Spring同时支持传统的RMI(使用java.rmi.Remote接口和java.rmi.RemoteException)和通过RMI调用器实现的透明远程调用(支持任何Java接口)。
Spring的HTTP调用器。Spring提供了一种特殊的允许通过HTTP进行Java串行化的远程调用策略,支持任意Java接口(就像RMI调用器)。相对应的支持类是 HttpInvokerProxyFactoryBean 和 HttpInvokerServiceExporter。
Hessian。通过 HessianProxyFactoryBean 和 HessianServiceExporter,可以使用Caucho提供的基于HTTP的轻量级二进制协议来透明地暴露服务。
Burlap。 Burlap是Caucho的另外一个子项目,可以作为Hessian基于XML的替代方案。Spring提供了诸如 BurlapProxyFactoryBean 和 BurlapServiceExporter 的支持类。
JAX RPC。Spring通过JAX-RPC为远程Web服务提供支持。
JMS(待实现)。
在讨论Spring对远程访问的支持时,我们将使用下面的域模型和对应的服务:
// Account domain object public class Account implements Serializable{ private String name; public String getName(); public void setName(String name) { this.name = name; } }
// Account service public interface AccountService { public void insertAccount(Account acc); public List getAccounts(String name); }
// Remote Account service public interface RemoteAccountService extends Remote { public void insertAccount(Account acc) throws RemoteException; public List getAccounts(String name) throws RemoteException; }
// ... and corresponding implement doing nothing at the moment public class AccountServiceImpl implements AccountService { public void insertAccount(Account acc) { // do something } public List getAccounts(String name) { // do something } }
我们将从使用RMI把服务暴露给远程客户端开始并探讨使用RMI的一些缺点。然后我们将演示一个使用Hessian的例子。
使用Spring的RMI支持,你可以通过RMI基础设施透明的暴露你的服务。设置好Spring的RMI支持后,你会看到一个和远程EJB接口类似的配置,只是没有对安全上下文传递和远程事务传递的标准支持。当使用RMI调用器时,Spring对这些额外的调用上下文提供了钩子,你可以在此插入安全框架或者定制的安全证书。
使用 RmiServiceExporter,我们可以把AccountService对象的接口暴露成RMI对象。可以使用 RmiProxyFactoryBean 或者在传统RMI服务中使用普通RMI来访问该接口。RmiServiceExporter 显式地支持使用RMI调用器暴露任何非RMI的服务。
当然,我们首先需要在Spring BeanFactory中设置我们的服务:
<bean id="accountService" class="example.AccountServiceImpl"> <!-- any additional properties, maybe a DAO? --> </bean>
然后,我们将使用 RmiServiceExporter 来暴露我们的服务:
<bean class="org.springframework.remoting.rmi.RmiServiceExporter"> <!-- does not necessarily have to be the same name as the bean to be exported --> <property name="serviceName" value="AccountService"/> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> <!-- defaults to 1099 --> <property name="registryPort" value="1199"/> </bean>
正如你所见,我们覆盖了RMI注册的端口号。通常,你的应用服务也会维护RMI注册,最好不要和它冲突。更进一步来说,服务名是用来绑定下面的服务的。所以本例中,服务绑定在 rmi://HOST:1199/AccountService。在客户端我们将使用这个URL来链接到服务。
注意:我们省略了一个属性,就是 servicePort 属性,它的默认值为0。 这表示在服务通信时使用匿名端口。当然如果你愿意的话,也可以指定一个不同的端口。
我们的客户端是一个使用AccountService来管理account的简单对象:
public class SimpleObject { private AccountService accountService; public void setAccountService(AccountService accountService) { this.accountService = accountService; } }
为了把服务连接到客户端上,我们将创建另一个单独的bean工厂,它包含这个简单对象和服务链接配置位:
<bean class="example.SimpleObject"> <property name="accountService" ref="accountService"/> </bean> <bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> <property name="serviceUrl" value="rmi://HOST:1199/AccountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
这就是我们在客户端为支持远程account服务所需要做的。Spring将透明的创建一个调用器并且通过RmiServiceExporter使得account服务支持远程服务。在客户端,我们用RmiProxyFactoryBean连接它。
Hessian提供一种基于HTTP的二进制远程协议。它是由Caucho创建的,可以在 http://www.caucho.com 找到更多有关Hessian的信息。
Hessian使用一个特定的Servlet通过HTTP进行通讯。使用Spring的DispatcherServlet,可以很容易的配置这样一个Servlet来暴露你的服务。首先我们要在你的应用里创建一个新的Servlet(下面来自web.xml文件):
<servlet> <servlet-name>remoting</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>remoting</servlet-name> <url-pattern>/remoting/*</url-pattern> </servlet-mapping>
你可能对Spring的DispatcherServlet很熟悉,这样你就知道,需要在 WEB-INF 目录里创建一个名为 remoting-servlet.xml(在你的servlet名后)的应用上下文。这个应用上下文将在下一节中里使用。
在新创建的 remoting-servlet.xml 应用上下文里,我们将创建一个HessianServiceExporter来暴露你的服务:
<bean id="accountService" class="example.AccountServiceImpl"> <!-- any additional properties, maybe a DAO? --> </bean> <bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter"> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
现在,我们准备在客户端连接服务了。不必显示指定处理器的映射,只要使用BeanNameUrlHandlerMapping把URL请求映射到服务上:所以,这个服务将在由bean名称指明的URL http://HOST:8080/remoting/AccountService 位置进行暴露。
使用 HessianProxyFactoryBean,我们可以在客户端连接服务。同样的方式对RMI示例也适用。我们将创建一个单独的bean工厂或者应用上下文,而后简单地指明下面的bean SimpleObject将使用AccountService来管理accounts:
<bean class="example.SimpleObject"> <property name="accountService" ref="accountService"/> </bean> <bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean"> <property name="serviceUrl" value="http://remotehost:8080/AccountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
这就是所有要做的。
我们在这里将不去仔细讨论Burlap,它是一个基于XML的Hessian替代方案。它的配置方法和上述Hessian的一样。只要把 Hessian 换成 Burlap 就行了。
Hessian和Burlap的一个优势是我们可以容易的使用HTTP基础认证,因为他们二者都是基于HTTP的。例如,普通HTTP Server安全机制可以通过使用 web.xml 安全特征来应用。通常,你不会为每个用户都建立不同的安全证书,而是在Hessian/BurlapProxyFactoryBean级别共享安全证书(类似一个JDBC数据源)。
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"> <property name="interceptors"> <list> <ref bean="authorizationInterceptor"/> </list> </property> </bean> <bean id="authorizationInterceptor" class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor"> <property name="authorizedRoles"> <list> <value>administrator</value> <value>operator</value> </list> </property> </bean>
这个例子里我们显式使用了BeanNameUrlHandlerMapping,并设置了一个拦截器,后者将只允许管理员和操作员调用这个应用上下文中提及的bean。
注意:当然,这个例子没有演示灵活的安全设施。考虑更多有关安全的问题时,请参阅 http://acegisecurity.sourceforge.net 处的Acegi Security System for Spring 。
和使用自身序列化机制的轻量级协议Burlap和Hessian相反,Spring HTTP调用器使用标准Java序列化机制来通过HTTP暴露业务。如果你的参数或返回值是复杂类型,并且不能通过Hessian和Burlap的序列化机制进行序列化,HTTP调用器就很有优势(参阅下一节,选择远程技术时的考虑)。
实际上,Spring可以使用J2SE提供的标准功能或Commons的HttpClient来实现HTTP调用。如果你需要更先进,更容易使用的功能,就使用后者。你可以参考 jakarta.apache.org/commons/httpclient。
为服务对象设置HTTP调用器和你在Hessian或Burlap中使用的方式类似。就象为Hessian支持提供的 HessianServiceExporter,Spring的HTTP调用器提供了 org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter。为了暴露 AccountService(上述的),使用下面的配置:
<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
同样,从客户端连接业务与你使用Hessian或Burlap时所做的很相似。使用代理,Spring可以将你调用的HTTP POST请求转换成被暴露服务的URL。
<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"> <property name="serviceUrl" value="http://remotehost:8080/AccountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
就象上面说的一样,你可以选择使用你想使用的HTTP客户端。缺省情况下,HttpInvokerPropxy使用J2SE的HTTP功能,但是你也可以通过设置httpInvokerRequestExecutor属性选择使用Commons HttpClient:
<property name="httpInvokerRequestExecutor"> <bean class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/> </property>
Spring支持:
使用JAX-RPC暴露服务
访问Web服务
除了上面所说的支持方法,你还可以用XFire xfire.codehaus.org 来暴露你的服务。XFire是一个轻量级的SOAP库,目前在Codehaus开发。
Spring对JAX-RPC Servlet的端点实现有个方便的基类 - ServletEndpointSupport。为暴露我们的Account服务,我们继承了Spring的ServletEndpointSupport类来实现业务逻辑,这里通常把调用委托给业务层。
/**
* JAX-RPC compliant RemoteAccountService implementation that simply delegates
* to the AccountService implementation in the root web application context.
*
* This wrapper class is necessary because JAX-RPC requires working with
* RMI interfaces. If an existing service needs to be exported, a wrapper that
* extends ServletEndpointSupport for simple application context access is
* the simplest JAX-RPC compliant way.
*
* This is the class registered with the server-side JAX-RPC implementation.
* In the case of Axis, this happens in "server-config.wsdd" respectively via
* deployment calls. The Web Service tool manages the life-cycle of instances
* of this class: A Spring application context can just be accessed here.
*/
public class AccountServiceEndpoint extends ServletEndpointSupport implements RemoteAccountService {
private AccountService biz;
protected void onInit() {
this.biz = (AccountService) getWebApplicationContext().getBean("accountService");
}
public void insertAccount(Account acc) throws RemoteException {
biz.insertAccount(acc);
}
public Account[] getAccounts(String name) throws RemoteException {
return biz.getAccounts(name);
}
}
AccountServletEndpoint需要在Spring中同一个上下文的web应用里运行,以获得对Spring的访问能力。如果使用Axis,把Axis的定义复制到你的web.xml中,并且在"server-config.wsdd"中设置端点(或使用发布工具)。参看JPetStore这个例子中OrderService是如何用Axis发布成一个Web服务的。
Spring有两个工厂bean用来创建Web服务代理,LocalJaxRpcServiceFactoryBean 和 JaxRpcPortProxyFactoryBean。前者只返回一个JAX-RPT服务类供我们使用。后者是一个全功能的版本,可以返回一个实现我们业务服务接口的代理。本例中,我们使用后者来为前面段落中暴露的AccountService端点创建一个代理。你将看到Spring对Web服务提供了极好的支持,只需要很少的代码 - 大多数都是通过类似下面的Spring配置文件:
<bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean"> <property name="serviceInterface" value="example.RemoteAccountService"/> <property name="wsdlDocumentUrl" value="http://localhost:8080/account/services/accountService?WSDL"/> <property name="namespaceUri" value="http://localhost:8080/account/services/accountService"/> <property name="serviceName" value="AccountService"/> <property name="portName" value="AccountPort"/> </bean>
serviceInterface 是客户端将要使用的远程业务接口。wsdlDocumentUrl 是WSDL文件的URL。Spring需要这些在启动时创建JAX-RPC服务。namespaceUri 对应到.wsdl文件中的targetNamespace。serviceName 对应到.wsdl文件中的service name。portName 对应到.wsdl文件中的端口号。
现在bean工厂将把Web服务暴露为 RemoteAccountService 接口,访问服务变得很容易。我们可以在Spring中这样组装起来:
<bean id="client" class="example.AccountClientImpl"> ... <property name="service" ref="accountWebService"/> </bean>
在客户端我们可以使用类似于普通类的方式来访问Web服务,区别是它抛出RemoteException异常。
public class AccountClientImpl { private RemoteAccountService service; public void setService(RemoteAccountService service) { this.service = service; } public void foo() { try { service.insertAccount(...); } catch (RemoteException ex) { // ouch ... } } }
由于Spring提供了自动转换成非受控异常的能力,我们可以不用考虑受控的RemoteException异常。这要求我们也提供一个非RMI接口,配置文件现在如下:
<bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean"> <property name="serviceInterface"> <value>example.AccountService</value> </property> <property name="portInterface"> <value>example.RemoteAccountService</value> </property> ... </bean>
这里 serviceInterface 已经改成我们目前的非RMI接口。我们的RMI接口现在使用属性 portInterface 进行定义。现在客户端代码可以不用处理 java.rmi.RemoteException 异常:
public class AccountClientImpl { private AccountService service; public void setService(AccountService service) { this.service = service; } public void foo() { service.insertAccount(...); } }
为了传递类似Account等复杂对象,我们必须在客户端注册bean映射。
Note | |
---|---|
在服务器端通常在server-config.wsdd中使用Axis进行bean映射注册。 |
我们将使用Axis在客户端注册bean映射。为此,我们需要继承一个Spring Bean工厂并通过编程注册这个bean映射。
public class AxisPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean { protected void postProcessJaxRpcService(Service service) { TypeMappingRegistry registry = service.getTypeMappingRegistry(); TypeMapping mapping = registry.createTypeMapping(); registerBeanMapping(mapping, Account.class, "Account"); registry.register("http://schemas.xmlsoap.org/soap/encoding/", mapping); } protected void registerBeanMapping(TypeMapping mapping, Class type, String name) { QName qName = new QName("http://localhost:8080/account/services/accountService", name); mapping.register(type, qName, new BeanSerializerFactory(type, qName), new BeanDeserializerFactory(type, qName)); } }
本节中,我们将注册自己的 javax.rpc.xml.handler.Handler 到Web服务代理,这样我们可以在SOAP消息被发送前执行定制的代码。javax.rpc.xml.handler.Handler 是一个回调接口。jarxpr.jar中有个方便的基类 - javax.rpc.xml.handler.GenericHandler 供我们继承使用:
public class AccountHandler extends GenericHandler { public QName[] getHeaders() { return null; } public boolean handleRequest(MessageContext context) { SOAPMessageContext smc = (SOAPMessageContext) context; SOAPMessage msg = smc.getMessage(); try { SOAPEnvelope envelope = msg.getSOAPPart().getEnvelope(); SOAPHeader header = envelope.getHeader(); ... } catch (SOAPException e) { throw new JAXRPCException(e); } return true; } }
我们现在要做的就是把AccountHandler注册到JAX-RPC服务,这样它可以在消息被发送前调用 handleRequest。Spring目前对注册处理方法还不提供声明式支持。所以我们必须使用编程方式。但是Spring中这很容易实现,我们只需继承相关的bean工厂类并覆盖专门为此设计的 postProcessJaxRpcService 方法:
public class AccountHandlerJaxRpcPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean { protected void postProcessJaxRpcService(Service service) { QName port = new QName(this.getNamespaceUri(), this.getPortName()); List list = service.getHandlerRegistry().getHandlerChain(port); list.add(new HandlerInfo(AccountHandler.class, null, null)); logger.info("Registered JAX-RPC Handler [" + AccountHandler.class.getName() + "] on port " + port); } }
最后,我们要记得更改Spring配置文件来使用我们的工厂bean。
<bean id="accountWebService" class="example.AccountHandlerJaxRpcPortProxyFactoryBean"> ... </bean>
XFire是一个Codehaus提供的轻量级SOAP库。在写作这个文档时(2005年3月)XFire还处于开发阶段。虽然Spring提供了稳定的支持,但是在未来应该会加入更多特性。暴露XFire是通过XFire自身带的context,这个context将和RemoteExporter风格的bean相结合,后者需要被加入到在你的WebApplicationContext中。
在所有这些允许你暴露服务的方法中,你都必须使用一个相关的WebApplicationContext来创建一个DispatcherServlet,这个WebApplicationContext包含将暴露的服务:
<servlet> <servlet-name>xfire</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> </servlet>
你还必须链接XFire配置。这是通过增加一个context文件到ContextLoaderListener(或者是Servlet)指定的 contextConfigLocations 参数中。这个配置文件在XFire jar中,当然这个jar文件应该放在你应用的classpath中。
<context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:org/codehaus/xfire/spring/xfire.xml </param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener>
在你加入一个Servlet映射后(映射 /* 到上面定义的XFire Servlet),你只需要增加一个额外的bean来暴露使用XFire的服务。例如,在 xfire-servlet.xml 中如下:
<beans> <bean name="/Echo" class="org.codehaus.xfire.spring.XFireExporter"> <property name="service" ref="echo"> <property name="serviceInterface" value="org.codehaus.xfire.spring.Echo"/> <property name="serviceBuilder" ref="xfire.serviceBuilder"/> <!-- the XFire bean is wired up in the xfire.xml file you've linked in earlier --> <property name="xfire" ref="xfire"/> </bean> <bean id="echo" class="org.codehaus.xfire.spring.EchoImpl"/> </beans>
XFire处理了其他的事情。它检查你的服务接口并产生一个WSDL文件。这里的部分文档来自XFire网站,要了解更多有关XFire Spring的集成请访问 docs.codehaus.org/display/XFIRE/Spring。
对远程接口不实现自动探测的主要原因是防止带来太多的远程调用。目标对象有可能实现的是类似InitializingBean或者DisposableBean的内部回调接口,而这些是不希望暴露给调用者的。
提供一个所有接口都被目标实现的代理通常和本地情况无关。但是当暴露一个远程服务时,你应该只暴露特定的用于远程使用的服务接口。除了内部回调接口,目标有可能实现了多个因为接口,而往往只有一个是用于远程使用的。出于这些原因,我们 要求 指定这样的服务接口。
这是在配置方便性和意外暴露内部方法具有的危险之间作的平衡。总是指明服务接口并不要花太大代价,并可以使你对于暴露指定方法更加安全。
这里提到的每种技术都有它的缺点。你在选择一种技术时,应该仔细考虑你的需要,你所暴露的服务和你在远程访问时传送的对象。
当使用RMI时,通过HTTP协议访问对象是不可能的,除非你用HTTP包裹RMI流。RMI是一种重量级的协议,因为它支持整个对象的序列化,当要求网络上传输复杂数据结构时这样的序列化是非常重要的。然而,RMI-JRMP只能绑定到Java客户端:它是一种Java-to-Java的远程访问解决方案。
如果你需要基于HTTP的远程访问而且还要求使用Java序列化,Spring的HTTP调用器是一个很好的选择。它和RMI调用器使用相同的基础设施,仅仅使用HTTP作为传输方式。注意HTTP调用器不仅只能用在Java-to-Java的远程访问,而且在客户端和服务器端都必须使用Spring。(Spring为非RMI接口提供的RMI调用器也要求客户端和服务器端都使用Spring)
当在异构环境中,Hessian和Burlap将可能极有价值。因为它们可以使用在非Java的客户端。然而,对非Java支持仍然是有限的。已知的问题包括含有延迟初始化的collection对象的Hibernate对象的序列化。如果你有一个这样的数据结构,应当考虑使用RMI或HTTP调用器,而不是Hessian。
在使用服务集群和需要JMS代理(JMS broker)来处理负载均衡,发现和自动-失败恢复服务时JMS是很有用的。缺省情况下,在使用JMS远程服务时使用Java序列化,但是JMS提供者也可以使用不同的机制例如XStream来让服务器用其他技术。
最后的一点是,相对于RMI,EJB有一个优点是它支持标准的基于角色的认证和授权,以及远程事务传递。用RMI调用器或HTTP调用器来支持安全上下文的传递是可能的,虽然这不由核心Spring提供:Spring提供了合适的钩子来插入第三方或定制的解决方案。