1. 程式人生 > >《Spring 5 官方文件》24. 使用Spring提供遠端和WEB服務

《Spring 5 官方文件》24. 使用Spring提供遠端和WEB服務

原文連結 譯者:xiuson

24.1 介紹

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的輕量級二進位制協議來透明地暴露服務。
  • JAX-WS。Spring通過JAX-WS為web服務提供遠端訪問支援。(JAX-WS: 從Java EE 5 和 Java 6開始引入,作為JAX-RPC的繼承者)
  • JMS。通過JmsInvokerServiceExporter和JmsInvokerProxyFacotryBean類,使用JMS作為底層協議來提供遠端服務。
  • AMQP。Spring AMQP專案支援AMQP作為底層協議來提供遠端服務。

在討論Spring的遠端服務功能時,我們將使用以下的域模型和對應的服務:


public class Account implements Serializable{
    private String name;

    public String getName(){
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

public interface AccountService {

    public void insertAccount(Account account);

    public List<Account> getAccounts(String name);

}

// the implementation doing nothing at the moment
public class AccountServiceImpl implements AccountService {

    public void insertAccount(Account acc) {
        // do something...
    }

    public List<Account> getAccounts(String name) {
        // do something...
    }

}

我們將從使用RMI把服務暴露給遠端客戶端開始,同時討論使用RMI的一些缺點。然後我們將繼續演示一個使用Hessian的例子。

24.2 使用RMI暴露服務

使用Spring的RMI支援,你可以通過RMI基礎架構透明地暴露你的服務。完成Spring的RMI設定後,你基本上具有類似於遠端EJB配 置,除了沒有對安全上下文傳遞和遠端事務傳遞的標準支援。當使用RMI呼叫器時,Spring對這些額外的呼叫上下文提供了鉤子,你可以在此插入安全框架 或者自定義的安全憑證。

24.2.1 使用RmiServiceExporter匯出服務

使用RmiServiceExporter,我們可以把AccountService物件的介面暴露成RMI物件。可以使用RmiProxyFactoryBean或者在傳統RMI服務中使用普通RMI來訪問該介面。RmiServiceExporter明確支援使用RMI呼叫器暴露任何非RMI的服務。

當然,我們首先需要在Spring容器中設定我們的服務:

<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來連結到服務。

Note:servicePort屬性被省略了(預設值為0).這表示在與服務通訊時將使用匿名埠.

24.2.2 在客戶端連結服務

我們的客戶端是一個使用AccountService來管理account的簡單物件:

public class SimpleObject {

    private AccountService accountService;

    public void setAccountService(AccountService accountService) {
        this.accountService = accountService;
    }

    // additional methods using the 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連線它。

24.3 使用Hessian通過HTTP遠端呼叫服務

Hessian提供一種基於HTTP的二進位制遠端協議。它由Caucho開發的,可以在 http://www.caucho.com 找到更多有關Hessian的資訊。

24.3.1 為Hessian和co.配置DispatcherServlet

Hessian使用一個自定義Servlet通過HTTP進行通訊。使用Spring的DispatcherServlet原理,從Spring Web MVC使用中可以看出,可以很容易的配置這樣一個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的名稱相匹配。

24.3.2 使用HessianServiceExporter暴露你的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匯出 DispatcherServlet’s mapping (as defined above): ’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’中為這個匯出器定義一個相應的servlet,也能得到同樣的結果:這個匯出器對映到request路徑/remoting/AccountService。注意這個servlet名稱需要與目標匯出器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>

24.3.3 在客戶端上鍊接服務

使用HessianProxyFactoryBean,我們可以在客戶端連結服務。與RMI示例一樣也適用相同的原理。我們將建立一個單獨的bean工廠或者應用上下文,並指明SimpleObject使用AccountService來管理accounts的以下bean:

<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>

24.3.4 對通過Hessian暴露的服務使用HTTP基本認證

Hessian的優點之一是,我們可以輕鬆應用HTTP基本身份驗證,因為這兩種協議都是基於HTTP的。你的正常HTTP 伺服器安全機制可以通過使用web.xml安全功能來應用。通常,你不會為每個使用者都建立不同的安全證書,而是在Hessian/BurlapProxyFactoryBean級別共享安全證書(類似一個JDBCDataSource)。

<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: 當然,這個例子並不表現出靈活的安全架構。有關安全性方面的更多選項,請檢視Spring Security專案http://projects.spring.io/spring-security/。

24.4 使用HTTP呼叫器暴露服務

與使用自身序列化機制的輕量級協議Hessian相反,Spring HTTP呼叫器使用標準Java序列化機制通過HTTP暴露業務。如果你的引數或返回值是複雜型別,並且不能通過Hessian的序列化機制進行序列化,HTTP呼叫器就很有優勢(請參閱下一節,以便在選擇遠端處理技術時進行更多考慮)。

在底層,Spring使用JDK提供的標準工具或Commons的HttpComponents來實現HTTP呼叫。如果你需要更先進和更易用的功能,請使用後者。你可以參考 hc.apache.org/httpcomponents-client-ga/ 以獲取更多資訊。

24.4.1 暴露服務物件

為服務物件設定HTTP呼叫器基礎架構類似於使用Hessian進行相同操作的方式。就象為Hessian支援提供的HessianServiceExporter,Spring的HTTP呼叫器提供了org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter。 為了在Spring Web MVC的DispatcherServlet中暴露AccountService(之前章節提及過), 需要在排程程式的應用程式上下文中使用以下配置:

<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
    <property name="service" ref="accountService"/>
    <property name="serviceInterface" value="example.AccountService"/>
</bean>

如Hessian章節部分所述,這個匯出器定義將通過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’中為該匯出器定義相應的servlet ,其中servlet名稱與目標匯出器的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>

如果你在一個servlet容器之外執行程式和使用Oracle的Java6, 那麼你可以使用內建的HTTP伺服器實現。你可以配置SimpleHttpServerFactoryBean和SimpleHttpInvokerServiceExporter在一起,像下面這個例子一樣:

<bean name="accountExporter"
        class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter">
    <property name="service" ref="accountService"/>
    <property name="serviceInterface" value="example.AccountService"/>
</bean>

<bean id="httpServer"
        class="org.springframework.remoting.support.SimpleHttpServerFactoryBean">
    <property name="contexts">
        <util:map>
            <entry key="/remoting/AccountService" value-ref="accountExporter"/>
        </util:map>
    </property>
    <property name="port" value="8080" />
</bean>

24.4.2 在客戶端連線服務

同樣,從客戶端連線業務與你使用Hessian所做的很相似。使用代理,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使用JDK的HTTP功能,但你也可以通過設定httpInvokerRequestExecutor屬性來使用ApacheHttpComponents客戶端:

<property name="httpInvokerRequestExecutor">
    <bean class="org.springframework.remoting.httpinvoker.HttpComponentsHttpInvokerRequestExecutor"/>
</property>

24.5 Web 服務

Spring提供了對標準Java Web服務API的全面支援:

  • 使用JAX-WS暴露Web服務
  • 使用JAX-WS訪問Web服務

除了在Spring Core中支援 JAX-WS,Spring portfolio也提供了一種特性Spring Web Services,一種為契約優先和文件驅動的web服務所提供的方案,強烈建議用來建立現代化的,面向未來的web服務。

24.5.1使用JAX- WS暴露基於servlet的web服務

Spring為JAX-WS servlet的端點實現提供了一個方便的基類 – SpringBeanAutowiringSupport. 為了暴露我們的AccountService,我們擴充套件Spring的SpringBeanAutowiringSupport類並在這裡實現了我們的業務邏輯,通常委派呼叫業務層。我們在Spring管理的bean裡面簡單地使用Spring的@Autowired 註解來表達這樣的依賴關係。

/**
 * 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);
    }

}

我們的AccountServletEndpoint需要和Spring在同一個上下文的web應用裡執行,以允許訪問Spring的功能。為JAX-WS servlet端點部署使用標準規約是Java EE 5 環境下的預設情況。

24.5.2 使用JAX-WS暴露單獨web服務

Oracle JDK 1.6附帶的內建JAX-WS provider 使用內建的HTTP伺服器來暴露web服務。Spring的SimpleJaxWsServiceExporter類檢測所有在Spring應用上下文中配置有@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:8080/"/>
</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 List<Account> getAccounts(String name) {
        return biz.getAccounts(name);
    }

}

24.5.3 使用JAX-WS RI的Spring支援來暴露服務

Oracle的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/

24.5.4 使用JAX-WS訪問web服務

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:8888/AccountServiceEndpoint?WSDL"/>
    <property name="namespaceUri" value="http://example/"/>
    <property name="serviceName" value="AccountService"/>
    <property name="portName" value="AccountServiceEndpointPort"/>
</bean>

serviceInterface是我們客戶端將使用的遠端業務介面。wsdlDocumentUrl是WSDL檔案的URL. Spring需要用它作為啟動點來建立JAX-WS服務。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(...);
    }
}

.

Note: 上面例子被稍微簡化了,因為JAX-WS需要端點介面及實現類來使用@WebService,@SOAPBinding等註解。 這意味著你不能簡單地使用普通的Java介面和實現來作為JAX-WS端點,你需要首先對它們進行相應的註解。這些需求詳情請查閱JAX-WS文件。

24.6 JMS

使用JMS來作為底層的通訊協議透明暴露服務也是可能的。Spring框架中對JMS的遠端支援也很基礎 – 它在同一執行緒和同一個非事務 Session上傳送和接收,這些吞吐量將非常依賴於實現。需要注意的是這些單執行緒和非事務的約束僅適用於Spring的JMS遠端處理支援。請參見 第26章, JMS (Java訊息服務),Spring對基於JMS的訊息的豐富支援。 下面的介面可同時用在服務端和客戶端。

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″?>

<?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.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>

24.6.1 服務端配置

在服務端你只需要使用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.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"});
    }

}

24.6.2 客戶端配置

客戶端只需要建立一個客戶端代理來實現上面的介面(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.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));
    }

}

24.7 AMQP

24.8 不實現遠端介面自動檢測

對遠端介面不實現自動探測的主要原因是為了避免向遠端呼叫者打開了太多的大門。目標物件有可能實現的是類似InitializingBean或者DisposableBean這樣的內部回撥介面,而這些是不希望暴露給呼叫者的。

提供一個所有介面都被目標實現的代理通常和本地情況無關。但是當暴露一個遠端服務時,你應該只暴露特定的用於遠端使用的服務介面。除了內部回撥介面,目標有可能實現了多個業務介面,而往往只有一個是用於遠端呼叫的。出於這些原因,我們要求指定這樣的服務介面。

這是在配置方便性和意外暴露內部方法的危險性之間作的權衡。始終指定一個服務介面並不需要花太大代價,並可以令控制具體方法暴露更加安全。

24.9 選擇技術時的注意事項

這裡提到的每種技術都有它的缺點。你在選擇一種技術時,應該仔細考慮你的需要和所暴露的服務及你在遠端訪問時傳送的物件。

當使用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介面。)

Hessian可以在異構環境中執行時提供重要的價值,因為它們明確允許非Java客戶端。然而,非Java支援仍然有限。已知問題包括將Hibernate物件與延遲初始化的集合相結合的序列化。如果您有這樣的資料模型,請考慮使用RMI或HTTP呼叫者而不是Hessian。

在使用服務叢集和需要JMS代理(JMS broker)來處理負載均衡及發現和自動-失敗恢復服務時JMS是很有用的。預設情況下,在使用JMS遠端服務時使用Java序列化,但是JMS提供者也可以使用不同的機制例如XStream來讓伺服器用其他技術。

最後但並非最不重要的是,EJB比RMI具有優勢,因為它支援標準的基於角色的身份認證和授權,以及遠端事務傳遞。用RMI呼叫器或HTTP呼叫器來支援安全上下文的傳遞是可能的,雖然這不由核心core Spring提供:Spring提供了合適的鉤子來插入第三方或定製的解決方案。

24.10 在客戶端訪問RESTful服務

RestTemplate是客戶端訪問RESTful服務的核心類。它在概念上類似於Spring中的其他模板類,例如JdbcTemplate、 JmsTemplate和其他Spring組合專案中發現的其他模板類。

RestTemplate’s behavior is customized by providing callback methods and configuring the `HttpMessageConverter用於將物件打包到HTTP請求體中,並將任何響應解包成一個物件。通常使用XML作為訊息格式,Spring提供了MarshallingHttpMessageConverter,它使用了的Object-to-XML框架,也是org.springframework.oxm包的一部分。這為你提供了各種各樣的XML到物件對映技術的選擇。

本節介紹如何使用RestTemplate它及其關聯 的HttpMessageConverters。

24.10.1 RestTemplate

在Java中呼叫RESTful服務通常使用助手類(如Apache HttpComponents)完成HttpClient。對於常見的REST操作,此方法的級別太低,如下所示。

String uri = "http://example.com/hotels/1/bookings";

PostMethod post = new PostMethod(uri);
String request = // create booking request content
post.setRequestEntity(new StringRequestEntity(request));

httpClient.executeMethod(post);

if (HttpStatus.SC_CREATED == post.getStatusCode()) {
    Header location = post.getRequestHeader("Location");
    if (location != null) {
        System.out.println("Created new booking at :" + location.getValue());
    }
}

RestTemplate提供了更高級別的方法,這些方法與六種主要的HTTP方法中的每一種相對應,這些方法使得呼叫許多RESTful服務成為一個單行和執行REST的最佳實踐。

Table 24.1. RestTemplate方法概述

RestTemplate方法名稱遵循命名約定,第一部分指出正在呼叫什麼HTTP方法,第二部分指出返回的內容。例如,該方法getForObject()將執行GET,將HTTP響應轉換為你選擇的物件型別並返回該物件。方法postForLocation() 將執行POST,將給定物件轉換為HTTP請求,並返回可以找到新建立的物件的響應HTTP Location頭。在異常處理HTTP請求的情況下,RestClientException型別的異常將被丟擲; 這個行為可以在RestTemplate通過插入另一個ResponseErrorHandler實現來改變。

exchange和execute方法是上面列出的更具體的方法的廣義版本,並且可以支援額外的組合和方法,例如HTTP PATCH。但是,請注意,底層HTTP庫還必須支援所需的組合。JDK HttpURLConnection不支援該PATCH方法,但Apache HttpComponents HttpClient4.2或更高版本支援。他們還能夠通過使用一個能夠捕獲和傳遞通用型別資訊的新類ParameterizedTypeReference來使得RestTemplate能夠讀取通用型別的HTTP響應資訊(例如List)。

物件通過HttpMessageConverter例項傳遞給這些方法並從這些方法返回被轉換為HTTP訊息。主要mime型別的轉換器預設註冊,但你也可以編寫自己的轉換器並通過messageConverters()實體屬性註冊它 。模板預設註冊的轉換器例項是ByteArrayHttpMessageConverter,StringHttpMessageConverter,FormHttpMessageConverter和SourceHttpMessageConverter。如果使用MarshallingHttpMessageConverter或者MappingJackson2HttpMessageConverter,你可以使用messageConverters()實體屬性覆蓋這些預設值。

每個方法以兩種形式使用URI模板引數,作為String可變長度引數或Map<String,String>。例如,使用可變長引數如下:

String result = restTemplate.getForObject(
    "http://example.com/hotels/{hotel}/bookings/{booking}", String.class,"42", "21");

使用一個Map<String,String>如下:

Map<String, String> vars = Collections.singletonMap("hotel", "42");
String result = restTemplate.getForObject(
        "http://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);

要建立一個例項,RestTemplate可以簡單地呼叫預設的無引數建構函式。這將使用java.net包中的標準Java類作為底層實現來建立HTTP請求。這可以通過指定實現來覆蓋ClientHttpRequestFactory。Spring提供了HttpComponentsClientHttpRequestFactory使用Apache HttpComponents HttpClient建立請求的實現。HttpComponentsClientHttpRequestFactory通過使用一個可以配置憑證資訊或連線池功能的org.apache.http.client.HttpClient例項來配置。

Note: HTTP請求的java.net實現可能會在訪問表示錯誤的響應狀態(例如401)時引發異常。如果這是一個問題,請切換到HttpComponentsClientHttpRequestFactory。

前面使用Apache HttpCOmponentsHttpClientdirectly的例子用RestTemplate重寫如下:

uri = "http://example.com/hotels/{id}/bookings";

RestTemplate template = new RestTemplate();

Booking booking = // create booking object

URI location = template.postForLocation(uri, booking, "1");

使用Apache HttpComponents, 而不是原生的java.net功能,構造RestTemplate如下:

RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());

.

Note: Apache HttpClient 支援gzip編碼,要使用這個功能,構造HttpCOmponentsClientHttpRequestFactory如下:
HttpClient httpClient = HttpClientBuilder.create().build();
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);

當execute方法被呼叫,通用的回撥介面是RequestCallback並且會被呼叫。

public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
        ResponseExtractor<T> responseExtractor, String... uriVariables)

// also has an overload with uriVariables as a Map<String, String>.

RequestCallback介面定義如下:

public interface RequestCallback {
 void doWithRequest(ClientHttpRequest request) throws IOException;
}

允許您操作請求標頭並寫入請求主體。當使用execute方法時,你不必擔心任何資源管理,模板將始終關閉請求並處理任何錯誤。有關使用execute方法及它的其他方法引數的含義的更多資訊,請參閱API文件。

使用URI

對於每個主要的HTTP方法,RestTemplate提供的變體使用String URI或java.net.URI作為第一個引數。
String URI變體將模板引數視為String變長引數或者一個Map<String,String>。他們還假定URL字串不被編碼且需要編碼。例如:

restTemplate.getForObject("http://example.com/hotel list", String.class);

將在 http://example.com/hotel%20list執行一個GET請求。這意味著如果輸入的URL字串已被編碼,它將被編碼兩次 – 即將 http://example.com/hotel%20list變為http://example.com/hotel%2520list。如果這不是預期的效果,則使用java.net.URI方法變體,假設URL已經被編碼,如果要重複使用單個(完全擴充套件)URI多次,通常也是有用的。

UriComponentsBuilder類可用於構建和編碼URI包括URI模板的支援。例如,你可以從URL字串開始:

UriComponents uriComponents = UriComponentsBuilder.fromUriString( "http://example.com/hotels/{hotel}/bookings/{booking}").build()
        .expand("42", "21")
        .encode();

URI uri = uriComponents.toUri();

或者分別制定每個URI元件:

UriComponents uriComponents = UriComponentsBuilder.newInstance()
        .scheme("http").host("example.com").path("/hotels/{hotel}/bookings/{booking}").build()
        .expand("42", "21")
        .encode();

URI uri = uriComponents.toUri();

處理請求和響應頭

除了上述方法之外,RestTemplate還具有exchange() 方法,可以用於基於HttpEntity 類的任意HTTP方法執行。
也許最重要的是,該exchange()方法可以用來新增請求頭和讀響應頭。例如:

HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("MyRequestHeader", "MyValue");
HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);

HttpEntity<String> response = template.exchange( "http://example.com/hotels/{hotel}",
        HttpMethod.GET, requestEntity, String.class, "42");

String responseHeader = response.getHeaders().getFirst("MyResponseHeader");
String body = response.getBody();

在上面的例子,我們首先準備了一個包含MyRequestHeader 頭的請求實體。然後我們檢索返回和讀取MyResponseHeader和訊息體。

Jackson JSON 檢視支援

可以指定一個 Jackson JSON檢視來系列化物件屬性的一部分,例如:

MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);
HttpEntity<MappingJacksonValue> entity = new HttpEntity<MappingJacksonValue>(value);
String s = template.postForObject("http://example.com/user", entity, String.class);

 24.10.2 HTTP 訊息轉換

通過HttpMessageConverters,物件傳遞到和從getForObject(),postForLocation(),和put()這些方法返回被轉換成HTTP請求和HTTP相應。HttpMessageConverter介面如下所示,讓你更好地感受它的功能:

public interface HttpMessageConverter<T> {

    // Indicate whether the given class and media type can be read by this converter.
    boolean canRead(Class<?> clazz, MediaType mediaType);

    // Indicate whether the given class and media type can be written by this converter.
    boolean canWrite(Class<?> clazz, MediaType mediaType);

    // Return the list of MediaType objects supported by this converter.
    List<MediaType> getSupportedMediaTypes();

    // Read an object of the given type from the given input message, and returns it.
    T read(Class<T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;

    // Write an given object to the given output message.
    void write(T t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;

}

框架中提供主要媒體(mime)型別的具體實現,預設情況下,通過RestTemplate在客戶端和 RequestMethodHandlerAdapter在伺服器端註冊。

HttpMessageConverter的實現下面章節中描述。對於所有轉換器,使用預設媒體型別,但可以通過設定supportedMediaTypesbean屬性來覆蓋。

StringHttpMessageConverter

一個HttpMessageConverter的實現,實現從HTTP請求和相應中讀和寫Strings。預設情況下,該轉換器支援所有的文字媒體型別(text/*),並用text/plain的Content-Type來寫。

FormHttpMessageConverter

一個HttpMessageConverter的實現,實現從HTTP請求和響應讀寫表單資料。預設情況下,該轉換器讀寫application/x-www-form-urlencoded媒體型別。表單資料被讀取並寫入MultiValueMap<String, String>。

ByteArrayHttpMessageConverter

一個HttpMessageConverter的實現,實現從HTTP請求和響應中讀取和寫入位元組陣列。預設情況下,此轉換器支援所有媒體型別(/),並使用其中的一種Content-Type進行寫入application/octet-stream。這可以通過設定supportedMediaTypes屬性和覆蓋getContentType(byte[])來重寫。

MarshallingHttpMessageConverter

一個HttpMessageConverter的實現,從org.springframework.oxm包中使用Spring的Marshaller和Unmarshaller抽象實現讀取和寫入XML。該轉換器需要Marshaller和Unmarshaller才能使用它。這些可以通過建構函式或bean屬性注入。預設情況下,此轉換器支援( text/xml)和(application/xml)。

MappingJackson2HttpMessageConverter

一個HttpMessageConverter的實現,使用Jackson XML擴充套件的ObjectMapper實現讀寫JSON。可以根據需要通過使用JAXB或Jackson提供的註釋來定製XML對映。當需要進一步控制時,XmlMapper 可以通過ObjectMapper屬性注入自定義,以便需要為特定型別提供自定義XML序列化器/反序列化器。預設情況下,此轉換器支援(application/xml)。

MappingJackson2XmlHttpMessageConverter

一個HttpMessageConverter的實現,可以使用Jackson XML擴充套件的XmlMapper讀取和寫入XML。可以根據需要通過使用JAXB或Jackson提供的註釋來定製XML對映。當需要進一步控制時,XmlMapper 可以通過ObjectMapper屬性注入自定義,以便需要為特定型別提供自定義XML序列化器/反序列化器。預設情況下,此轉換器支援(application/xml)。

SourceHttpMessageConverter

一個HttpMessageConverter的實現,從HTTP請求和響應中讀寫 javax.xml.transform.Source。僅支援DOMSource、SAXSource和StreamSource。預設情況下,此轉換器支援(text/xml)和(application/xml)。

BufferedImageHttpMessageConverter

一個HttpMessageConverter的實現,從HTTP請求和響應中讀寫java.awt.image.BufferedImage。此轉換器讀寫Java I/O API支援的媒體型別。

24.10.3 非同步RestTemplate

Web應用程式通常需要查詢外部REST服務。當為這些需求擴張應用程式時,HTTP和同步呼叫的性質帶來挑戰:可能會阻塞多個執行緒,等待遠端HTTP響應。

AsyncRestTemplate和第24.10.1節“RestTemplate”的API非常相似; 請 參見表24.1“RestTemplate方法概述”。這些API之間的主要區別是AsyncRestTemplate返回ListenableFuture 封裝器而不是具體的結果。

前面的RestTemplate例子翻譯成:

// async call
Future<ResponseEntity<String>> futureEntity = template.getForEntity(
    "http://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");

// get the concrete result - synchronous call
ResponseEntity<String> entity = futureEntity.get();

ListenableFuture 接受完成回撥:

ListenableFuture<ResponseEntity<String>> futureEntity = template.getForEntity(
    "http://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");

// register a callback
futureEntity.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
    @Override
    public void onSuccess(ResponseEntity<String> entity) {
        //...
    }

    @Override
    public void onFailure(Throwable t) {
        //...
    }
});

.

Note: 預設AsyncRestTemplate建構函式為執行HTTP請求註冊一個SimpleAsyncTaskExecutor 。當處理大量短命令請求時,執行緒池的TaskExecutor實現ThreadPoolTaskExecutor 可能是一個不錯的選擇。

有關更多詳細資訊,參考ListenableFuture的javadoc and AsyncTestTmeplate的javadoc.