28. 使用 Spring 进行远程和 Web 服务

28.1 Introduction

Spring 具有集成类,用于使用各种技术来远程化支持。远程支持简化了由常规(Spring)POJO 实施的启用远程服务的开发。当前,Spring 支持以下远程技术:

  • 远程方法调用(RMI)。通过使用RmiProxyFactoryBeanRmiServiceExporter,Spring 既支持传统的 RMI(具有java.rmi.Remote接口和java.rmi.RemoteException),也支持通过 RMI 调用程序进行透明远程处理(具有任何 Java 接口)。

    • Spring 的 HTTP 调用程序*。 Spring 提供了一种特殊的远程处理策略,该策略允许通过 HTTP 进行 Java 序列化,从而支持任何 Java 接口(就像 RMI 调用程序一样)。相应的支持类别为HttpInvokerProxyFactoryBeanHttpInvokerServiceExporter
  • 黑森 State。通过使用 Spring 的HessianProxyFactoryBeanHessianServiceExporter,您可以使用 Caucho 提供的基于 HTTP 的轻量级二进制协议透明地公开您的服务。

  • 粗麻布。 Burlap 是 Caucho 基于 XML 的替代 Hessian 的替代品。 Spring 提供了诸如BurlapProxyFactoryBeanBurlapServiceExporter之类的支持类。

    • JAX-WS *。 Spring 通过 JAX-WS(Java EE 5 和 Java 6 中引入的 JAX-RPC 的继承者)为 Web 服务提供远程支持。
    • JMS *。通过JmsInvokerServiceExporterJmsInvokerProxyFactoryBean类支持使用 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 将服务公开给远程 Client 端,并讨论使用 RMI 的缺点。然后,我们将 continue 展示一个使用 Hessian 作为协议的示例。

28.2 使用 RMI 公开服务

使用 Spring 对 RMI 的支持,您可以通过 RMI 基础结构透明地公开服务。进行了此设置之后,除了不存在对安全上下文传播或远程事务传播的标准支持这一事实之外,您基本上具有与远程 EJB 相似的配置。当使用 RMI 调用程序时,Spring 确实为此类附加调用上下文提供了钩子,因此您可以在此处插入安全框架或自定义安全凭证。

28.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 链接到 Client 端的服务。

Note

servicePort属性已被省略(默认为 0)。这意味着将使用匿名端口与服务进行通信。

28.2.2 在 Client 端链接服务

我们的 Client 是使用AccountService来 Management 帐户的简单对象:

public class SimpleObject {

    private AccountService accountService;

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

    // additional methods using the accountService

}

为了在 Client 端上链接服务,我们将创建一个单独的 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>

这就是我们支持 Client 端上的远程帐户服务所需要做的一切。 Spring 将透明地创建一个调用程序,并通过RmiServiceExporter远程启用帐户服务。在 Client 端,我们使用RmiProxyFactoryBean进行链接。

28.3 使用粗麻布或粗麻布通过 HTTP 远程调用服务

Hessian 提供了一个基于 HTTP 的二进制远程协议。它由 Caucho 开发,有关 Hessian 本身的更多信息可在http://www.caucho.com找到。

28.3.1 为 Hessian 和 co。连接 DispatcherServlet。

Hessian 通过 HTTP 进行通信,并使用自定义 servlet 进行通信。使用 Spring Web MVC 用法所知的 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 名称之后)的 Spring 容器配置资源。下一部分将使用应用程序上下文。

或者,考虑使用 Spring 的简单HttpRequestHandlerServlet。这使您可以将远程导出器定义嵌入到根应用程序上下文中(默认情况下为'WEB-INF/applicationContext.xml'),并使用单个 Servlet 定义指向特定的导出器 bean。在这种情况下,每个 servlet 名称都需要与其目标导出器的 bean 名称相匹配。

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

现在,我们准备在 Client 端链接服务。没有指定显式的处理程序 Map,而是将请求 URLMap 到服务,因此将使用BeanNameUrlHandlerMapping:因此,将在包含DispatcherServlet的 Map(如上定义):'http://HOST:8080/remoting/AccountService'内,以通过其 bean 名称指示的 URL 导出服务。

或者,在您的根应用程序上下文中创建一个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'中为此 Export 者定义一个相应的 servlet,最终结果相同:Export 者被 Map 到请求路径/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>

28.3.3 在 Client 端上链接服务

使用HessianProxyFactoryBean,我们可以在 Client 端链接服务。与 RMI 示例相同的原理适用。我们将创建一个单独的 bean 工厂或应用程序上下文,并提及以下 Bean,其中SimpleObject使用AccountServiceManagement 帐户:

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

28.3.4 使用粗麻布

我们不会在此处详细讨论 Burlap(相当于 Hessian 的 XML),因为它的配置和设置方式与上述 Hessian 变体完全相同。只需将Hessian替换为Burlap,就可以开始了。

28.3.5 将 HTTP 基本身份验证应用于通过 Hessian 或 Burlap 公开的服务

Hessian 和 Burlap 的优点之一是我们可以轻松地应用 HTTP 基本身份验证,因为这两种协议都是基于 HTTP 的。例如,可以使用web.xml安全功能轻松地应用常规的 HTTP 服务器安全机制。通常,您在这里不使用每个用户的安全凭证,而是在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并设置了一个拦截器,仅允许 Management 员和操作员调用此应用程序上下文中提到的 bean。

Note

当然,此示例未显示灵活的安全基础结构。有关安全性的更多选项,请查看位于http://projects.spring.io/spring-security/的 Spring Security 项目。

28.4 使用 HTTP 调用程序公开服务

与使用自己的苗条序列化机制的轻量协议 Burlap 和 Hessian 相反,Spring HTTP 调用程序使用标准的 Java 序列化机制通过 HTTP 公开服务。如果您的参数和返回类型是无法使用 Hessian 和 Burlap 使用的序列化机制进行序列化的复杂类型,则这将具有巨大的优势(选择远程处理技术时,请参考下一节以获得更多注意事项)。

在幕后,Spring 使用 JDK 或 Apache HttpComponents提供的标准功能来执行 HTTP 调用。如果您需要更高级且更易于使用的功能,请使用后者。有关更多信息,请参考hc.apache.org/httpcomponents-client-ga/

Warning

注意由于不安全的 Java 反序列化而导致的漏洞:在反序列化步骤中,操纵的 Importing 流可能导致服务器上有害的代码执行。因此,不要将 HTTP 调用者终结点暴露给不受信任的 Client 端,而应该暴露给您自己的服务之间。通常,我们强烈建议您使用其他任何消息格式(例如 JSON)。

如果您担心由 Java 序列化引起的安全漏洞,请考虑在核心 JVM 级别上使用通用序列化筛选器机制,该机制最初是为 JDK 9 开发的,但同时又反向移植到 JDK 8、7 和 6:https://blogs.oracle.com/java-platform-group/entry/incoming_filter_serialization_data_a http://openjdk.java.net/jeps/290

28.4.1 公开服务对象

为服务对象设置 HTTP 调用程序基础结构与使用 Hessian 或 Burlap 进行操作的方式非常相似。就像 Hessian 支持提供HessianServiceExporter一样,Spring 的 HttpInvoker 支持提供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>

此类导出程序的定义将通过DispatcherServlet的标准 Map 工具公开,如 Hessian 一节中所述。

或者,在您的根应用程序上下文中(例如'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'中为此 Export 者定义一个相应的 servlet,其 servlet 名称与目标 Export 者的 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 的 Java 6,则可以使用内置的 HTTP 服务器实现。您可以将SimpleHttpServerFactoryBeanSimpleHttpInvokerServiceExporter一起配置,如下例所示:

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

28.4.2 在 Client 端链接服务

同样,从 Client 端链接服务非常类似于使用 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>

如前所述,您可以选择要使用的 HTTPClient 端。默认情况下,HttpInvokerProxy使用 JDK 的 HTTP 功能,但是您也可以通过设置httpInvokerRequestExecutor属性来使用 Apache HttpComponentsClient 端:

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

28.5Web Service

Spring 提供对标准 Java Web 服务 API 的全面支持:

  • 使用 JAX-WS 公开 Web 服务

  • 使用 JAX-WS 访问 Web 服务

除了在 Spring Core 中对 JAX-WS 的库存支持之外,Spring 产品组合还具有SpringWeb Service,这是一种针对 Contract 优先,文档驱动的 Web 服务的解决方案-强烈建议用于构建现代的,面向 Future 的 Web 服务。

28.5.1 使用 JAX-WS 公开基于 servlet 的 Web 服务

Spring 为 JAX-WS servlet 端点实现SpringBeanAutowiringSupport提供了一个方便的 Base Class。为了公开AccountService,我们扩展 Spring 的SpringBeanAutowiringSupport类并在此处实现我们的业务逻辑,通常将调用委派给业务层。我们将仅使用 Spring 的@Autowired注解来表达对 SpringManagement 的 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);
    }

}

我们的AccountServiceEndpoint需要在与 Spring 上下文相同的 Web 应用程序中运行,以允许访问 Spring 的设施。在 Java EE 5 环境中,默认情况下就是这种情况,使用用于 JAX-WS servlet 端点部署的标准协定。有关详细信息,请参见 Java EE 5 Web 服务教程。

28.5.2 使用 JAX-WS 导出独立的 Web 服务

Oracle JDK 随附的内置 JAX-WS 提供程序也支持使用 JDK 中包含的内置 HTTP 服务器公开 Web 服务。 Spring 的SimpleJaxWsServiceExporter检测到 Spring 应用程序上下文中所有带有@WebServiceComments 的 bean,并通过默认的 JAX-WS 服务器(JDK HTTP 服务器)导出它们。

在这种情况下,端点实例被定义和 Management 为 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派生而来,但不必这样做,因为此处的端点是完全由 SpringManagement 的 bean。这意味着端点实现可能如下所示,而没有声明任何超类-并且仍然采用 Spring 的@Autowired配置 Comments:

@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);
    }

}

28.5.3 使用 JAX-WS RI 的 Spring 支持导出 Web 服务

作为 GlassFish 项目的一部分开发的 Oracle JAX-WS RI,将 Spring 支持作为其 JAX-WS Commons 项目的一部分。这允许将 JAX-WS 端点定义为 SpringManagement 的 Bean,类似于上一节中讨论的独立模式-但这次是在 Servlet 环境中。 请注意,这在 Java EE 5 环境中不可移植;它主要用于 Tomcat 等非 EE 环境,并将 JAX-WS RI 嵌入为 Web 应用程序的一部分.

与导出基于 servlet 的端点的标准样式的不同之处在于,这里将由 SpringManagement 端点实例本身的生命周期,并且在web.xml中仅定义一个 JAX-WS servlet。使用标准的 Java EE 5 样式(如上所示),每个服务端点将有一个 servlet 定义,每个端点通常都委派给 Spring Bean(通过使用@Autowired,如上所示)。

请查看https://jax-ws-commons.java.net/spring/以获取有关设置和使用方式的详细信息。

28.5.4 使用 JAX-WS 访问 Web 服务

Spring 提供了两个工厂 bean 来创建 JAX-WS Web 服务代理,即LocalJaxWsServiceFactoryBeanJaxWsPortProxyFactoryBean。前者只能返回 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是 Client 将使用的我们的业务界面。 wsdlDocumentUrl是 WSDL 文件的 URL。 Spring 需要这个启动时间来创建 JAX-WS 服务。 namespaceUri对应于.wsdl 文件中的 targetNamespace。 serviceName对应于.wsdl 文件中的服务名称。 portName对应于.wsdl 文件中的端口名称。

现在,访问 Web 服务非常容易,因为我们有一个供其使用的 bean 工厂,它将把它公开为AccountService接口。我们可以在 Spring 将其连接起来:

<bean id="client" class="example.AccountClientImpl">
    ...
    <property name="service" ref="accountWebService"/>
</bean>

从 Client 端代码,我们可以像访问普通类一样访问 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 etcComments 对端点接口和实现类进行 Comments。这意味着您不能(轻松)使用纯 Java 接口和实现类作为 JAX-WS 端点工件。您需要首先对它们进行 Comments。查看 JAX-WS 文档以获取有关这些需求的详细信息。

28.6 JMS

使用 JMS 作为基础通信协议,也可以透明地公开服务。 Spring 框架中的 JMS 远程支持是非常基本的-它在same thread和“相同的非事务性” Session中进行发送和接收,因此,吞吐量将非常依赖于实现。请注意,这些单线程和非事务性约束仅适用于 Spring 的 JMS * remoting 支持。请参阅第 30 章,JMS(Java 消息服务),以获取有关 Spring 对基于 JMS 的消息*的丰富支持的信息。

服务器和 Client 端均使用以下接口。

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 + "]");
    }

}

此配置文件包含在 Client 端和服务器上共享的 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.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>

28.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"});
    }

}

28.6.2Client 端配置

Client 端只需要创建一个 Client 端代理即可实现约定的接口(CheckingAccountService)。在以下 bean 定义的后面创建的结果对象可以注入到其他 Client 端对象中,并且代理将负责通过 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));
    }

}

28.7 AMQP

有关更多信息,请参考Spring AMQP 参考文档“使用 AMQP 进行远程处理”部分

28.8 远程接口未实现自动检测

对于远程接口,不会自动检测已实现接口的主要原因是为了避免为远程调用者打开太多门。目标对象可能会实现内部回调接口,例如InitializingBeanDisposableBean,而这些接口将不希望向调用者公开。

在本地情况下,提供具有目标所实现的所有接口的代理通常无关紧要。但是,在导出远程服务时,您应该公开特定的服务接口,以及用于远程使用的特定操作。除了内部回调接口之外,目标还可以实现多个业务接口,其中只有一个用于远程公开。由于这些原因,我们需要指定这样的服务接口。

这是配置便利性与内部方法意外暴露风险之间的折衷方案。始终指定服务接口不会花费太多精力,这使您可以安全地控制特定方法的使用。

28.9 选择技术时的注意事项

这里介绍的每种技术都有其缺点。选择一项技术时,应仔细考虑您的需求,要公开的服务以及将通过电线发送的对象。

使用 RMI 时,除非通过隧道传输 RMI 流量,否则无法通过 HTTP 协议访问对象。 RMI 是一个重量级协议,因为它支持全对象序列化,这在使用需要通过网络进行序列化的复杂数据模型时非常重要。但是,RMI-JRMP 与 JavaClient 端绑定:它是 Java 到 Java 的远程解决方案。

如果您需要基于 HTTP 的远程处理而且还依赖 Java 序列化,那么 Spring 的 HTTP 调用程序是一个不错的选择。它与 RMI 调用程序共享基本的基础结构,仅使用 HTTP 作为传输。请注意,HTTP 调用程序不仅限于 Java 到 Java 远程处理,而且还限于 Client 端和服务器端的 Spring。 (后者也适用于非 RMI 接口的 Spring RMI 调用程序.)

在异类环境中运行时,Hessian 和/或 Burlap 可能会提供重要的价值,因为它们明确允许使用非 JavaClient 端。但是,非 Java 支持仍然有限。已知的问题包括 Hibernate 对象的序列化以及延迟初始化的集合。如果您有这样的数据模型,请考虑使用 RMI 或 HTTP 调用程序而不是 Hessian。

JMS 可用于提供服务集群,并允许 JMS 代理负责负载平衡,发现和自动故障转移。默认情况下:使用 JMS 远程处理时使用 Java 序列化,但是 JMS 提供程序可以使用其他机制进行有线格式设置,例如 XStream,以允许服务器以其他技术实现。

最后但并非最不重要的一点是,EJB 比 RMI 更具优势,因为它支持基于角色的标准身份验证和授权以及远程事务传播。尽管核心 Spring 并没有提供 RMI 调用程序或 HTTP 调用程序来支持安全上下文传播,但也有可能:尽管这里有适当的钩子可用于插入第三方或自定义解决方案。

28.10 在 Client 端上访问 RESTful 服务

RestTemplate是 Client 端访问 RESTful 服务的核心类。从概念上讲,它类似于 Spring 中的其他模板类,例如JdbcTemplateJmsTemplate以及其他 Spring 产品组合项目中的其他模板类。 RestTemplate's behavior is customized by providing callback methods and configuring theHttpMessageConverter用于将对象封送至 HTTP 请求正文中,并将任何响应封送回对象。由于通常将 XML 用作消息格式,因此 Spring 提供了MarshallingHttpMessageConverter,该MarshallingHttpMessageConverter使用了org.springframework.oxm`包中的 Object-to-XML 框架。这为您提供了多种 XML 到对象 Map 技术的选择。

本节介绍如何使用RestTemplate及其关联的HttpMessageConverters

28.10.1 RestTemplate

通常,使用诸如 Apache HttpComponents HttpClient之类的帮助器类来完成 Java 中 RESTful 服务的调用。对于常见的 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 最佳实践。

Note

RestTemplate 具有异步计数器:请参见第 28.10.3 节“异步 RestTemplate”

表 28.1. RestTemplate 方法概述

HTTP MethodRestTemplate Method
DELETEdelete
GETgetForObject getForEntity
HEADheadForHeaders(字符串 url,字符串…uriVariables)
OPTIONSoptionsForAllow(字符串 url,字符串…uriVariables)
POSTpostForLocation(字符串 url,对象请求,字符串…uriVariables) postForObject(字符串 url,对象请求,Class<T> responseType,字符串…uriVariables)
PUTput(字符串 url,对象请求,字符串…uriVariables)
补丁和其他exchange execute

RestTemplate方法的名称遵循命名约定,第一部分指示正在调用的 HTTP 方法,第二部分指示返回的内容。例如,方法getForObject()将执行 GET,将 HTTP 响应转换为您选择的对象类型,然后返回该对象。方法postForLocation()将执行 POST,将给定的对象转换为 HTTP 请求,然后返回响应 HTTP LocationHeaders,可以在其中找到新创建的对象。在处理 HTTP 请求的异常情况下,将抛出RestClientException类型的异常。可以通过将另一个ResponseErrorHandler实现插入RestTemplate来更改此行为。

exchangeexecute方法是它们上方列出的更具体方法的通用版本,可以支持其他组合和方法,例如 HTTP 修补程序。但是,请注意,基础 HTTP 库也必须支持所需的组合。 JDK HttpURLConnection不支持PATCH方法,但是 Apache HttpComponents HttpClient 版本 4.2 或更高版本支持。它们还使RestTemplate使用新的ParameterizedTypeReference类(能够捕获和传递通用类型信息)来读取对通用类型(例如List<Account>)的 HTTP 响应。

HttpMessageConverter实现将传递给这些方法和从这些方法返回的对象与 HTTP 消息进行相互转换。默认情况下会注册主要 MIME 类型的转换器,但您也可以覆盖默认值,并通过messageConverters() bean 属性注册自定义转换器。默认转换器是ByteArrayHttpMessageConverterStringHttpMessageConverterResourceHttpMessageConverterSourceHttpMessageConverter以及AllEncompassingFormHttpMessageConverter以及一些特定于提供商的转换器: MappingJackson2HttpMessageConverter当 Classpath 中存在 Jackson 时。

每个方法都采用两种形式的 URI 模板参数,即String可变长度参数或Map<String,String>。例如,

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

使用变长参数和

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

使用Map<String,String>

要创建RestTemplate的实例,您可以简单地调用默认的 no-arg 构造函数。这将使用java.net包中的标准 Java 类作为基础实现来创建 HTTP 请求。可以通过指定ClientHttpRequestFactory的实现来覆盖它。 Spring 提供了使用 Apache HttpComponents HttpClient创建请求的实现HttpComponentsClientHttpRequestFactoryHttpComponentsClientHttpRequestFactory是使用org.apache.http.client.HttpClient的实例配置的,而该实例又可以使用凭据信息或连接池功能进行配置。

Tip

请注意,访问代表错误的响应状态(例如 401)时,HTTP 请求的java.net实现可能会引发异常。如果这是一个问题,请改用HttpComponentsClientHttpRequestFactory

上面的示例使用 Apache HttpComponents HttpClient直接重写为使用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());

Tip

Apache HttpClient 支持 gzip 编码。要使用它,请构建一个HttpComponentsClientHttpRequestFactory,如下所示:

HttpClient httpClient = HttpClientBuilder.create().build();
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);

通用回调接口是RequestCallback,并在调用 execute 方法时调用。

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

并允许您操纵请求 Headers 并写入请求正文。使用 execute 方法时,您不必担心任何资源 Management,模板将始终关闭请求并处理任何错误。有关使用 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。这意味着,如果 Importing 的 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();

处理请求和响应 Headers

除上述方法外,RestTemplate还具有exchange()方法,该方法可用于基于HttpEntity类的任意 HTTP 方法执行。

也许最重要的是,exchange()方法可用于添加请求 Headers 和读取响应 Headers。例如:

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();

在上面的示例中,我们首先准备一个包含MyRequestHeaderHeaders 的请求实体。然后,我们检索响应,并读取MyResponseHeader和正文。

Jackson JSON 视图支持

可以指定JacksonJSON 视图以仅序列化对象属性的子集。例如:

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

28.10.2 HTTP 消息转换

传递给方法getForObject()postForLocation()put()并从中返回的对象由HttpMessageConverters转换为 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)类型的具体实现,默认情况下,它们在 Client 端的RestTemplate和服务器端的RequestMethodHandlerAdapter注册。

以下各节介绍了HttpMessageConverter的实现。对于所有转换器,都使用默认媒体类型,但是可以通过设置supportedMediaTypes bean 属性来覆盖

StringHttpMessageConverter

可以从 HTTP 请求和响应读取和写入字符串的HttpMessageConverter实现。默认情况下,此转换器支持所有文本媒体类型(text/*),并以Content-Typetext/plain进行写入。

FormHttpMessageConverter

可以从 HTTP 请求和响应中读取和写入表单数据的HttpMessageConverter实现。默认情况下,此转换器读取和写入媒体类型application/x-www-form-urlencoded。从MultiValueMap<String, String>读取表格数据并将其写入。

ByteArrayHttpMessageConverter

可以从 HTTP 请求和响应读取和写入字节数组的HttpMessageConverter实现。默认情况下,此转换器支持所有媒体类型(*/*),并以Content-Typeapplication/octet-stream进行写入。可以通过设置supportedMediaTypes属性并覆盖getContentType(byte[])来覆盖它。

MarshallingHttpMessageConverter

可以使用org.springframework.oxm包中的 Spring 的MarshallerUnmarshaller抽象来读取和写入 XML 的HttpMessageConverter实现。该转换器需要使用MarshallerUnmarshaller才能使用。这些可以通过构造函数或 bean 属性注入。默认情况下,此转换器支持(text/xml)和(application/xml)。

MappingJackson2HttpMessageConverter

可以使用 Jackson 的ObjectMapper读取和写入 JSON 的HttpMessageConverter实现。可以根据需要使用 Jackson 提供的 Comments 自定义 JSONMap。当需要进一步控制时,可以通过ObjectMapper属性注入自定义ObjectMapper,以用于需要为特定类型提供自定义 JSON 序列化器/反序列化器的情况。默认情况下,此转换器支持(application/json)。

MappingJackson2XmlHttpMessageConverter

可以使用Jackson XMLextensionsXmlMapper读写 XML 的HttpMessageConverter实现。可以根据需要通过使用 JAXB 或 Jackson 提供的 Comments 来定制 XMLMap。当需要进一步控制时,对于需要为特定类型提供自定义 XML 序列化器/反序列化器的情况,可以通过ObjectMapper属性注入自定义XmlMapper。默认情况下,此转换器支持(application/xml)。

SourceHttpMessageConverter

可以从 HTTP 请求和响应中读取和写入javax.xml.transform.SourceHttpMessageConverter实现。仅支持DOMSourceSAXSourceStreamSource。默认情况下,此转换器支持(text/xml)和(application/xml)。

BufferedImageHttpMessageConverter

可以从 HTTP 请求和响应中读取和写入java.awt.image.BufferedImageHttpMessageConverter实现。该转换器读取和写入 Java I/O API 支持的媒体类型。

28.10.3 异步 RestTemplate

那时,Web 应用程序通常需要查询外部 REST 服务。在为满足这些需求扩展应用程序时,HTTP 和同步调用的本质可能导致挑战:可能会阻塞多个线程,await 远程 HTTP 响应。

AsyncRestTemplate第 28.10.1 节“ RestTemplate”的 API 非常相似;参见表 28.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构造函数注册一个SimpleAsyncTaskExecutor以执行 HTTP 请求。当处理大量短期请求时,像ThreadPoolTaskExecutor这样的线程池 TaskExecutor 实现可能是一个不错的选择。

有关更多详细信息,请参见ListenableFuture javadocsAsyncRestTemplate javadocs