28. 使用Spring的远程处理和Web服务

28.1 简介

Spring提供了使用各种技术进行远程支持的集成类。远程支持简化了通常(Spring)POJO实现的远程启用服务的开发。目前,Spring支持以下远程技术:

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

  • Spring的HTTP调用者。 Spring提供了一种特殊的远程处理策略,允许通过HTTP进行Java序列化,支持任何Java接口(就像RMI调用程序一样)。相应的支持类是 HttpInvokerProxyFactoryBeanHttpInvokerServiceExporter

  • 黑森州。通过使用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开始向远程客户端公开服务,并谈谈使用RMI的缺点。然后我们将继续展示使用Hessian作为协议的示例。

28.2 使用RMI公开服务

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

28.2.1 使用RmiServiceExporter导出服务

使用 RmiServiceExporter ,我们可以将AccountService对象的接口公开为RMI对象。在传统的RMI服务的情况下,可以使用 RmiProxyFactoryBean 或通过普通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链接到客户端的服务。

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

28.2.2 在客户端的服务中进行链接

我们的客户端是使用 AccountService 来管理帐户的简单对象:

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>

这就是我们需要做的就是支持客户端上的远程帐户服务。 Spring 会透明地创建一个调用者并通过 RmiServiceExporter 远程启用帐户服务。在客户端,我们使用 RmiProxyFactoryBean 链接它。

28.3 使用Hessian或Burlap通过HTTP远程调用服务

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

28.3.1 为Hessian和co。连接DispatcherServlet。

Hessian通过HTTP进行通信,并使用自定义servlet进行通信。使用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' 的Spring容器配置资源(在您的servlet名称之后)。应用程序上下文将在下一节中使用。

或者,考虑使用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>

现在我们已准备好在客户端的服务中进行链接。没有指定显式的处理程序映射,将请求URL映射到服务,因此将使用 BeanNameUrlHandlerMapping :因此,服务将在包含 DispatcherServlet 的映射(如上定义)中通过其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' 中为此导出器定义相应的servlet,结果相同:导出器将映射到请求路径 /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 链接客户端上的服务

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

<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 使用粗麻布

我们不会在这里详细讨论基于XML的Hessian等价物Burlap,因为它的配置和设置方式与上面解释的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 并设置一个拦截器,只允许管理员和操作员调用此应用程序上下文中提到的bean。

当然,此示例未显示灵活的安全基础结构。有关安全性的更多选项,请查看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/

注意由于不安全的Java反序列化导致的漏洞:在反序列化步骤中,操作的输入流可能导致服务器上不需要的代码执行。因此,不要将HTTP调用程序 endpoints 暴露给不受信任的客户端,而只是在您自己的服务之间。通常,我们强烈建议使用任何其他消息格式(例如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 的标准制图设施公开,如Hessian部分所述。

或者,在根应用程序上下文中创建 HttpInvokerServiceExporter (例如,在 'WEB-INF/applicationContext.xml' 中):

<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的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 在客户端的服务中进行链接

同样,来自客户端的服务链接非常类似于使用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 使用JDK的HTTP功能,但您也可以通过设置 httpInvokerRequestExecutor 属性来使用Apache HttpComponents 客户端:

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

28.5 网络服务

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

  • 使用JAX-WS公开Web服务

  • 使用JAX-WS访问Web服务

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

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

Spring为JAX-WS servlet endpoints 实现提供了方便的基类 - SpringBeanAutowiringSupport 。为了公开我们的 AccountService ,我们扩展了Spring的 SpringBeanAutowiringSupport 类并在这里实现了我们的业务逻辑,通常将调用委托给业务层。我们将简单地使用Spring的 @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);
    }

}

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

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

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

在这种情况下, endpoints 实例是作为Spring bean本身定义和管理的;它们将在JAX-WS引擎中注册,但它们的生命周期将取决于Spring应用程序上下文。这意味着可以将诸如显式依赖注入之类的Spring功能应用于 endpoints 实例。当然,通过 @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 但不必,因为 endpoints 是一个完全由Spring管理的bean。这意味着 endpoints 实现可能如下所示,没有声明任何超类 - 并且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);
    }

}

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

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

导出基于servlet的 endpoints 的标准样式的不同之处在于 endpoints 实例本身的生命周期将由Spring管理,并且只有一个JAX-WS servlet在 web.xml 中定义。使用标准的Java EE 5样式(如上所示),每个服务 endpoints 都有一个servlet定义,每个 endpoints 通常委托给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 endpoints 创建代理(再次):

<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服务非常简单,因为我们有一个bean工厂,它将它公开为 AccountService 接口。我们可以在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要求 endpoints 接口和实现类使用@WebService,@ SOAPBinding等注释进行注释。这意味着您不能(轻松地)将纯Java接口和实现类用作JAX-WS endpoints 工件;你需要先对它们进行相应的注释。有关这些要求的详细信息,请查看JAX-WS文档。

28.6 JMS

还可以使用JMS作为底层通信协议透明地公开服务。 Spring Framework中的JMS远程支持是非常基本的 - 它在 same thread 上发送和接收,并且在相同的非事务性 Session 中,因此吞吐量将非常依赖于实现。请注意,这些单线程和非事务约束仅适用于Spring的JMS远程支持。有关Spring对基于JMS的消息传递的丰富支持的信息,请参阅 Chapter 30, JMS (Java Message Service)

服务器和客户端都使用以下接口。

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

}

28.7 AMQP

有关更多信息,请参阅 Spring AMQP Reference Document 'Spring Remoting with AMQP' section

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

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

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

这是配置便利性与内部方法意外暴露风险之间的权衡。始终指定服务接口不是太费力,并且使您在特定方法的受控暴露方面处于安全的位置。

28.9 选择技术时的注意事项

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

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

Spring的HTTP调用程序很好选择是否需要基于HTTP的远程处理,但也依赖于Java序列化。它与RMI调用者共享基本的基础结构,只使用HTTP作为传输。请注意,HTTP调用程序不仅限于Java-to-Java远程处理,还包括客户端和服务器端的Spring。 (后者也适用于Spring的非RMI接口的RMI调用程序。)

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

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

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

28.10 在客户端上访问RESTful服务

RestTemplate 是客户端访问RESTful服务的核心类。它在概念上类似于Spring中的其他模板类,例如 JdbcTemplateJmsTemplate 以及其他Spring组合项目中的其他模板类。 RestTemplate’s behavior is customized by providing callback methods and configuring theHttpMessageConverter用于将对象编组到HTTP请求主体中,并将任何响应解组回到对象中。由于通常使用XML作为消息格式,因此Spring提供了MarshallingHttpMessageConverter,它使用了作为org.springframework.oxm` 包的一部分的Object-to-XML框架。这为您提供了多种XML到对象映射技术的选择。

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

28.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最佳实践。

RestTemplate有一个异步计数器部分:请参见第28.10.3节“异步RestTemplate”。

Table 28.1. Overview of RestTemplate methods

HTTP方法RestTemplate方法
删除delete
GETgetForObject getForEntity
HEADheadForHeaders(String url, String… uriVariables)
选项optionsForAllow(String url, String… uriVariables)
POSTpostForLocation(String url, Object request, String… uriVariables) postForObject(String url, Object request, Class responseType, String… uriVariables)
PUTput(String url, Object request, String…uriVariables)
PATCH和其他exchange execute

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

exchangeexecute 方法是上面列出的更具体方法的通用版本,并且可以支持其他组合和方法,例如, HTTP PATCH。但请注意,底层HTTP库还必须支持所需的组合。 JDK HttpURLConnection 不支持 PATCH 方法,但Apache HttpComponents HttpClient 4.2或更高版本支持。它们还允许 RestTemplate 使用 ParameterizedTypeReference 读取对泛型类型的HTTP响应(例如 List<Account> ),这是一个允许捕获和传递泛型类型信息的新类。

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

每种方法都以两种形式获取URI模板参数,或者作为a 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 的实例,只需调用默认的无参数构造函数即可。这将使用 java.net 包中的标准Java类作为创建HTTP请求的底层实现。可以通过指定 ClientHttpRequestFactory 的实现来覆盖它。 Spring提供了使用Apache HttpComponents HttpClient 来创建请求的实现 HttpComponentsClientHttpRequestFactoryHttpComponentsClientHttpRequestFactory 使用 org.apache.http.client.HttpClient 实例进行配置,该实例又可以配置凭据信息或连接池功能。

请注意,HTTP请求的java.net实现可能在访问表示错误的响应的状态时引发异常(例如401)。如果这是一个问题,请切换到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());

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

并允许您操作请求标头并写入请求正文。使用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 Views支持

可以指定 Jackson JSON View 仅序列化对象属性的子集。例如:

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)类型的具体实现在框架中提供,默认情况下在客户端注册 RestTemplate ,在服务器端注册 RequestMethodHandlerAdapter

HttpMessageConverter 的实现将在以下部分中介绍。对于所有转换器,使用默认媒体类型,但可以通过设置 supportedMediaTypes bean属性来覆盖它

StringHttpMessageConverter

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

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的 MarshallerUnmarshaller 抽象来读写XML。该转换器需要 MarshallerUnmarshaller 才能使用。这些可以通过构造函数或bean属性注入。默认情况下,此转换器支持( text/xml )和( application/xml )。

MappingJackson2HttpMessageConverter

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

MappingJackson2XmlHttpMessageConverter

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

SourceHttpMessageConverter

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

BufferedImageHttpMessageConverter

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

28.10.3 异步RestTemplate

Web应用程序通常需要查询外部REST服务。在为这些需求扩展应用程序时,HTTP和同步调用的本质可能会带来挑战:可能会阻止多个线程,等待远程HTTP响应。

AsyncRestTemplateSection 28.10.1, “RestTemplate” 的API非常相似;见 Table 28.1, “Overview of RestTemplate methods” 。这些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) {
        //...
    }
});

默认的AsyncRestTemplate构造函数注册了一个SimpleAsyncTaskExecutor来执行HTTP请求。在处理大量短期请求时,像ThreadPoolTaskExecutor这样的线程池TaskExecutor实现可能是一个不错的选择。

有关详细信息,请参阅 ListenableFuture javadocsAsyncRestTemplate javadocs

Updated at: 5 months ago
VII. IntegrationTable of content29. 企业JavaBeans(EJB)集成
Comment
You are not logged in.

There are no comments.