1.1. 簡介
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基於XML用來替代Hessian的項目。Spring提供了諸如 BurlapProxyFactoryBean 和 BurlapServiceExporter 的支持類。
-
JAX RPC。Spring通過JAX-RPC(J2EE 1.4's wweb service API)為Web services提供遠程服務支持。
-
JAX-WS. Spring通過(在Java EE 5和Java 6中引入的JAX-RPC繼承)為遠程Web Services提供支持。
-
JMS. 通過JmsInvokerServiceExporter和JmsInvokerProxyFactoryBean使用JMS做為底層協議提供遠程服務.
在討論Spring對遠程訪問的支持時,我們將使用下面的域模型和對應的服務:
public class Account implements Serializable{ private String name; public String getName(); public void setName(String name) { this.name = name; } }
public interface AccountService { public void insertAccount(Account account); public List getAccounts(String name); }
public interface RemoteAccountService extends Remote { public void insertAccount(Account account) throws RemoteException; public List getAccounts(String name) throws RemoteException; }
// 該實現目前什么事情也不做 public class AccountServiceImpl implements AccountService { public void insertAccount(Account acc) { // 做一些事情…… } public List getAccounts(String name) { // 做一些事情…… } }
我們將從使用RMI把服務暴露給遠程客戶端開始,同時探討RMI的一些缺點。然后我們將演示一個使用Hessian的例子。
使用Spring的RMI支持,你可以通過RMI基礎設施透明的暴露你的服務。設置好Spring的RMI支持后,你會看到一個和遠程EJB接口類似的配置,只是沒有對安全上下文傳遞和遠程事務傳遞的標准支持。當使用RMI調用器時,Spring對這些額外的調用上下文提供了鈎子,你可以在此插入安全框架或者定制的安全證書。
使用RmiServiceExporter,我們可以把AccountService對象的接口暴露成RMI對象。可以使用 RmiProxyFactoryBean 或者在傳統RMI服務中使用普通RMI來訪問該接口。RmiServiceExporter 顯式地支持使用RMI調用器暴露任何非RMI的服務。
當然,我們首先需要在Spring容器中設置我們的服務:
<bean id="accountService" class="example.AccountServiceImpl">
<!--其他屬性,或者一個DAO對象?-->
</bean>
然后我們要使用RmiServiceExporter來暴露我們的服務:
<bean class="org.springframework.remoting.rmi.RmiServiceExporter"> <!-- 不一定要與要輸出的bean同名--> <property name="serviceName" value="AccountService"/> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> <!-- 默認為1199--> <property name="registryPort" value="1199"/> </bean>
正如你所見,我們覆蓋了RMI注冊的端口號。通常你的應用服務器也會維護RMI注冊,最好不要和它沖突。更進一步來說,服務名是用來綁定服務的。所以本例中,服務綁定在rmi://HOST:1199/AccountService。在客戶端我們將使用這個URL來鏈接到服務。
![]() |
Note |
---|---|
servicePort 屬性被省略了(它的默認值為0).這表示在與服務通信時將使用匿名端口. |
我們的客戶端是一個使用AccountService來管理account的簡單對象:
public class SimpleObject { private AccountService accountService; public void setAccountService(AccountService accountService) { this.accountService = accountService; } }
為了把服務連接到客戶端上,我們將創建一個單獨的Spring容器,包含這個簡單對象和鏈接配置位的服務:
<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在Web MVC中就常用的 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名稱后) 的Spring容器配置上下文。這個應用上下文將在下一節中里使用。
另外,可以考慮使用Spring中簡單的HttpRequestHandlerServlet。這允許你在根應用上下文(默認是'WEB-INF/applicationContext.xml')中插入遠程exporter定義。每個servlet定義指向特定的exporter bean。在這種情況下,每個servlet的名稱需要和目標exporter bean的名稱相匹配。
在新創建的 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 位置進行暴露。
另外一種選擇, 在你的根應用上下文中創建一個 HessianServiceExporter (比如在'WEB-INF/applicationContext.xml'中):
<bean name="accountExporter" class="org.springframework.remoting.caucho.HessianServiceExporter"> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
在后一情況下, 在'web.xml'中為exporter定義一個相應的servlet,也能得到同樣的結果:這個exporter映射到request路徑/remoting/AccountService。注意這個servlet名稱需要與目標exporter bean的名稱相匹配。
<servlet> <servlet-name>accountExporter</servlet-name> <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>accountExporter</servlet-name> <url-pattern>/remoting/AccountService</url-pattern> </servlet-mapping>
使用HessianProxyFactoryBean,我們可以在客戶端連接服務。同樣的方式對RMI示例也適用。我們將創建一個單獨的bean工廠或者應用上下文,而后簡單地指明下面的beanSimpleObject將使用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/remoting/AccountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
我們將不會詳細討論Burlap,它是一個基於XML的Hessian替代方案。它的配置和構建方法和上述Hessian的一樣。只要把 Hessian 換成 Burlap 就行了。
Hessian和Burlap的一個優勢是我們可以容易的使用HTTP Basic認證,因為二者都是基於HTTP的。例如,普通HTTP Server安全機制可以通過使用 web.xml 安全特性來應用。通常,你不會為每個用戶都建立不同的安全證書,而是在Hessian/BurlapProxyFactoryBean級別共享安全證書(類似一個JDBC DataSource)。
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"> <property name="interceptors" ref="authorizationInterceptor"/> </bean> <bean id="authorizationInterceptor" class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor"> <property name="authorizedRoles" value="administrator,operator"/> </bean>
這個例子里我們顯式使用了BeanNameUrlHandlerMapping,並設置了一個攔截器,只允許管理員和操作員調用這個應用上下文中提及的bean。
![]() |
Note |
---|---|
當然,這個例子沒有演示靈活的安全設施。考慮更多有關安全的問題時,請參閱 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。
為了在Spring Web MVC DispatcherServlet中暴露AccountService (如上所述), 需要在dispatcher的應用上下文中使用以下配置:
<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
和在Hessian章節講的一樣,這個exporter定義將通過 DispatcherServlet標准的映射工具暴露出來。
做為可選項, 在你的根應用上下文中(比如'WEB-INF/applicationContext.xml')創建一個HttpInvokerServiceExporter :
<bean name="accountExporter" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
另外,在'web.xml'中為這個exporter定義一個相應的servlet,其名稱與目標exporter bean的名稱相匹配:
<servlet> <servlet-name>accountExporter</servlet-name> <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>accountExporter</servlet-name> <url-pattern>/remoting/AccountService</url-pattern> </servlet-mapping>
同樣,從客戶端連接業務與你使用Hessian或Burlap時所做的很相似。使用代理,Spring可以將你調用的HTTP POST請求轉換成被暴露服務的URL。
<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"> <property name="serviceUrl" value="http://remotehost:8080/remoting/AccountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean>
就象上面說的一樣,你可以選擇使用你想使用的HTTP客戶端。缺省情況下,HttpInvokerProxy使用J2SE的HTTP功能,但是你也可以通過設置httpInvokerRequestExecutor屬性選擇使用Commons HttpClient:
<property name="httpInvokerRequestExecutor"> <bean class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/> </property>
Spring為標准Java web服務API提供了全面的支持:
-
使用JAX-RPC暴露web服務
-
使用JAX-RPC訪問web服務
-
使用JAX-WS暴露web服務
-
使用JAX-WS訪問web服務
![]() |
Note |
---|---|
為什么有2個標准的Java web服務APIs? JAX-RPC 1.1 在J2EE 1.4 中是標准的web服務API。正像其名稱所示,它關注於RPC綁定而且在最近幾年越來越不流行。最終被Java EE 5中的JAX-WS 2.0所取代,JAX-WS 2.0不但在綁定方面更靈活,而且也是完全基於annotation的。JAX-WS 2.1也被包含在Java 6中(更詳細的說是在Sun JDK 1.6.0_04和更高版本中,低版本的Sun JDK 1.6.0包含JAX-WS 2.0),它與JDK內置的HTTP服務器集成。 Spring 同時支持兩個標准Java web服務API。選擇誰主要看運行平台:在JDK 1.4 / J2EE 1.4上,唯一的選擇是JAX-RPC。在Java EE 5 / Java 6上顯然應該選JAX-WS。運行Java 5的J2EE 1.4環境上,你可以選擇插入一個JAX-WS provider;請查看你的J2EE服務器文檔。 |
除了在Spring Core中支持JAX-RPC and JAX-WS,Spring portfolio也提供了一種特性Spring Web Services,一個為優先授權和文檔驅動的web服務所提供的方案 - 非常建議用來創建高級並具有前瞻性的web服務。XFire是最后但不是唯一的Spring內置支持可以讓你將Spring管理的bean暴露為web服務的方式。
Spring為JAX-RPC servlet的端點實現提供了一個方便的基類 - ServletEndpointSupport. 未來暴露我們的 AccountService我們擴展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 dedicated * endpoint classes. 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 engine manages the lifecycle of instances * of this class: A Spring application context can just be accessed here. */import org.springframework.remoting.jaxrpc.ServletEndpointSupport;
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,把AxisServlet定義復制到你的'web.xml'中,並且在'server-config.wsdd'中設置端點(或使用發布工具)。參看JPetStore這個例子中OrderService是如何用Axis發布成一個Web服務的。
Spring提供了兩個工廠bean用來創建Web服務代理,LocalJaxRpcServiceFactoryBean 和 JaxRpcPortProxyFactoryBean。前者只返回一個JAX-RPC服務類供我們使用。后者是一個全功能的版本,可以返回一個實現我們業務服務接口的代理。本例中,我們使用后者來為前面段落中暴露的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文件中的服務名。 portName 對應.wsdl文件中的端口號。
現在我們可以很方便的訪問web服務,因為我們有一個可以將它暴露為RemoteAccountService接口的bean工廠。我們可以在Spring中這樣使用:
<bean id="client" class="example.AccountClientImpl"> ... <property name="service" ref="accountWebService"/> </bean>
從客戶端代碼上看,除了它拋出RemoteException,我們可以把這個web服務當成一個普通的類進行訪,。
public class AccountClientImpl {
private RemoteAccountService service;
public void setService(RemoteAccountService service) {
this.service = service;
}
public void foo() {
try {
service.insertAccount(...);
}
catch (RemoteException ex) {
// ouch
}
}
}
我們可以不檢查受控異常RemoteException,因為Spring將它自動轉換成相應的非受控異常RemoteException。這也需要我們提供一個非RMI的接口。現在配置文件如下:
<bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean"> <property name="serviceInterface" value="example.AccountService"/> <property name="portInterface" value="example.RemoteAccountService"/> </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(...); } }
請注意你也可以去掉"portInterface"部分並指定一個普通業務接口作為"serviceInterface"。這樣JaxRpcPortProxyFactoryBean將自動切換到JAX-RPC "動態調用接口", 不使用固定端口存根來進行動態調用。這樣做的好處是你甚至不需要使用一個RMI相關的Java接口(比如在非Java的目標web服務中);你只需要一個匹配的業務接口。查看JaxRpcPortProxyFactoryBean的javadoc來了解運行時實行的細節。
T為了傳遞類似Account等復雜對象,我們必須在客戶端注冊bean映射。
![]() |
Note |
---|---|
在服務器端通常在'server-config.wsdd'中使用Axis進行bean映射注冊。 |
我們將使用Axis在客戶端注冊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消息被發送前執行定制的代碼。Handler是一個回調接口。jaxrpc.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 ex) { throw new JAXRPCException(ex); } return true; } }
我們現在要做的就是把AccountHandler注冊到JAX-RPC服務,這樣它可以在消息被發送前調用 handleRequest(..)。Spring目前對注冊處理方法還不提供聲明式支持,所以我們必須使用編程方式。但是Spring中這很容易實現,我們只需覆寫專門為此設計的 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 AccountHandler on port " + port); } }
最后,我們要記得更改Spring配置文件來使用我們的工廠bean:
<bean id="accountWebService" class="example.AccountHandlerJaxRpcPortProxyFactoryBean"> ... </bean>
Spring為JAX-WS servlet端點實現提供了一個方便的基類 - SpringBeanAutowiringSupport。要暴露我們的AccountService接口,我們可以擴展Spring的SpringBeanAutowiringSupport類並實現我們的業務邏輯,通常把調用交給業務層。我們將簡單的使用Spring 2.5的@Autowired注解來聲明依賴於Spring管理的bean。
/** * JAX-WS compliant AccountService implementation that simply delegates * to the AccountService implementation in the root web application context. * * This wrapper class is necessary because JAX-WS requires working with dedicated * endpoint classes. If an existing service needs to be exported, a wrapper that * extends SpringBeanAutowiringSupport for simple Spring bean autowiring (through * the @Autowired annotation) is the simplest JAX-WS compliant way. * * This is the class registered with the server-side JAX-WS implementation. * In the case of a Java EE 5 server, this would simply be defined as a servlet * in web.xml, with the server detecting that this is a JAX-WS endpoint and reacting * accordingly. The servlet name usually needs to match the specified WS service name. * * The web service engine manages the lifecycle of instances of this class. * Spring bean references will just be wired in here. */import org.springframework.web.context.support.SpringBeanAutowiringSupport;
@WebService(serviceName="AccountService")
public class AccountServiceEndpoint extends SpringBeanAutowiringSupport {
@Autowired
private AccountService biz;
@WebMethod
public void insertAccount(Account acc) {
biz.insertAccount(acc);
}
@WebMethod
public Account[] getAccounts(String name) {
return biz.getAccounts(name);
}
}
為了能夠讓Spring上下文使用Spring設施,我們的AccountServletEndpoint類需要運行在同一個web應用中。在Java EE 5環境中這是默認的情況,它使用JAX-WS servlet端點安裝標准契約。詳情請參閱Java EE 5 web服務教程。
Sun JDK 1.6提供的內置JAX-WS provider 使用內置的HTTP服務器來暴露web服務。Spring的SimpleJaxWsServiceExporter類檢測所有在Spring應用上下文中配置的l@WebService 注解bean,然后通過默認的JAX-WS服務器(JDK 1.6 HTTP服務器)來暴露它們。
在這種場景下,端點實例將被作為Spring bean來定義和管理。它們將使用JAX-WS來注冊,但其生命周期將一直跟隨Spring應用上下文。這意味着Spring的顯示依賴注入可用於端點實例。當然通過@Autowired來進行注解驅動的注入也可以正常工作。
<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter"> <property name="baseAddress" value="http://localhost:9999/"/> </bean> <bean id="accountServiceEndpoint" class="example.AccountServiceEndpoint"> ... </bean> ...
AccountServiceEndpoint類可能源自Spring的 SpringBeanAutowiringSupport類,也可能不是。因為這里的端點是由Spring完全管理的bean。這意味着端點實現可能像下面這樣沒有任何父類定義 - 而且Spring的@Autowired配置注解仍然能夠使用:
@WebService(serviceName="AccountService") public class AccountServiceEndpoint { @Autowired private AccountService biz; @WebMethod public void insertAccount(Account acc) { biz.insertAccount(acc); } @WebMethod public Account[] getAccounts(String name) { return biz.getAccounts(name); } }
Sun的JAX-WS RI被作為GlassFish項目的一部分來開發,它使用了Spring支持來作為JAX-WS Commons項目的一部分。這允許把JAX-WS端點作為Spring管理的bean來定義。這與前面章節討論的單獨模式類似 - 但這次是在Servlet環境中。注意這在Java EE 5環境中是不可遷移的,建議在沒有EE的web應用環境如Tomcat中嵌入JAX-WS RI。
與標准的暴露基於servlet的端點方式不同之處在於端點實例的生命周期將被Spring管理。這里在web.xml將只有一個JAX-WS servlet定義。在標准的Java EE 5風格中(如上所示),你將對每個服務端點定義一個servlet,每個服務端點都代理到Spring bean (通過使用@Autowired,如上所示)。
關於安裝和使用詳情請查閱https://jax-ws-commons.dev.java.net/spring/。
類似JAX-RPC支持,Spring提供了2個工廠bean來創建JAX-WS web服務代理,它們是LocalJaxWsServiceFactoryBean和JaxWsPortProxyFactoryBean。前一個只能返回一個JAX-WS服務對象來讓我們使用。后面的是可以返回我們業務服務接口的代理實現的完整版本。這個例子中我們使用后者來為AccountService端點再創建一個代理:
<bean id="accountWebService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean"> <property name="serviceInterface" value="example.AccountService"/> <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文件中的服務名。 portName 對應.wsdl文件中的端口號。
現在我們可以很方便的訪問web服務,因為我們有一個可以將它暴露為AccountService接口的bean工廠。我們可以在Spring中這樣使用:
<bean id="client" class="example.AccountClientImpl"> ... <property name="service" ref="accountWebService"/> </bean>
從客戶端代碼上我們可以把這個web服務當成一個普通的類進行訪問:
public class AccountClientImpl { private AccountService service; public void setService(AccountService service) { this.service = service; } public void foo() { service.insertAccount(...); } }
注意: 上面被稍微簡化了,因為JAX-WS需要端點接口及實現類來使用@WebService, @SOAPBinding等注解。 這意味着你不能簡單的使用普通的Java接口和實現來作為JAX-WS端點,你需要首先對它們進行相應的注解。這些需求詳情請查閱JAX-WS文檔。
XFire是一個Codehaus提供的輕量級SOAP庫。暴露XFire是通過XFire自帶的context,這個context將和RemoteExporter風格的bean相結合,后者需要被加入到在你的WebApplicationContext中。對於所有讓你來暴露服務的方法,你需要創建一個DispatcherServlet類並有相應的WebApplicationContext來封裝你將要暴露的服務:
<servlet> <servlet-name>xfire</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet>
你還必須鏈接XFire配置。這是通過增加一個context文件到由ContextLoaderListener(或者ContextLoaderServlet)加載的 contextConfigLocations 參數中。
<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.remoting.XFireExporter">
<property name="serviceInterface" value="org.codehaus.xfire.spring.Echo"/>
<property name="serviceBean">
<bean class="org.codehaus.xfire.spring.EchoImpl"/>
</property>
<!-- the XFire bean is defined in the xfire.xml file -->
<property name="xfire" ref="xfire"/>
</bean>
</beans>
XFire處理了其他的事情。它檢查你的服務接口並產生一個WSDL文件。這里的部分文檔來自XFire網站,要了解更多有關XFire Spring的集成請訪問docs.codehaus.org/display/XFIRE/Spring。
使用JMS來作為底層的通信協議透明暴露服務也是可能的。Spring框架中對JMS的遠程支持也很基礎 - 它在同一線程和同一個非事務 Session上發送和接收,這些吞吐量將非常依賴於實現。
The following interface is used on both the server and the client side.
下面的接口可同時用在服務端和客戶端。
package com.foo; public interface CheckingAccountService { public void cancelAccount(Long accountId); }
對於上面接口的使用在服務的端簡單實現如下。
package com.foo; public class SimpleCheckingAccountService implements CheckingAccountService { public void cancelAccount(Long accountId) { System.out.println("Cancelling account [" + accountId + "]"); } }
這個包含JMS設施的bean的配置文件可同時用在客戶端和服務端。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://ep-t43:61616"/> </bean> <bean id="queue" class="org.apache.activemq.command.ActiveMQQueue"> <constructor-arg value="mmm"/> </bean> </beans>
在服務端你只需要使用JmsInvokerServiceExporter來暴露服務對象。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="checkingAccountService" class="org.springframework.jms.remoting.JmsInvokerServiceExporter"> <property name="serviceInterface" value="com.foo.CheckingAccountService"/> <property name="service"> <bean class="com.foo.SimpleCheckingAccountService"/> </property> </bean> <bean class="org.springframework.jms.listener.SimpleMessageListenerContainer"> <property name="connectionFactory" ref="connectionFactory"/> <property name="destination" ref="queue"/> <property name="concurrentConsumers" value="3"/> <property name="messageListener" ref="checkingAccountService"/> </bean> </beans>
package com.foo; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Server { public static void main(String[] args) throws Exception { new ClassPathXmlApplicationContext(new String[]{"com/foo/server.xml", "com/foo/jms.xml"}); } }
客戶端僅僅需要創建一個客戶端代理來實現上面的接口(CheckingAccountService)。根據后面的bean定義創建的結果對象可以被注入到其它客戶端對象中,而這個代理會負責通過JMS將調用轉發到服務端。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="checkingAccountService" class="org.springframework.jms.remoting.JmsInvokerProxyFactoryBean"> <property name="serviceInterface" value="com.foo.CheckingAccountService"/> <property name="connectionFactory" ref="connectionFactory"/> <property name="queue" ref="queue"/> </bean> </beans>
package com.foo; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Client { public static void main(String[] args) throws Exception { ApplicationContext ctx = new ClassPathXmlApplicationContext( new String[] {"com/foo/client.xml", "com/foo/jms.xml"}); CheckingAccountService service = (CheckingAccountService) ctx.getBean("checkingAccountService"); service.cancelAccount(new Long(10)); } }
你可能也希望研究Lingo 項目提供的支持,它(引用到主頁) “... 是一個基於輕量級POJO的遠程核消息代碼庫,它使用並擴展了Spring框架的遠程代碼庫以支持JMS。”
對遠程接口不實現自動探測的主要原因是防止產生太多的遠程調用。目標對象有可能實現的是類似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)
在使用服務集群和需要JMS代理(JMS broker)來處理負載均衡及發現和自動-失敗恢復服務時JMS是很有用的。缺省情況下,在使用JMS遠程服務時使用Java序列化,但是JMS提供者也可以使用不同的機制例如XStream來讓服務器用其他技術。
最后但不僅限於此,相對於RMI,EJB有一個優點是它支持標准的基於角色的認證和授權,以及遠程事務傳遞。用RMI調用器或HTTP調用器來支持安全上下文的傳遞是可能的,雖然這不由核心Spring提供:Spring提供了合適的鈎子來插入第三方或定制的解決方案。