Integration

参考文档的这一部分涵盖了 Spring Framework 与许多 Java EE(及相关)技术的集成。

1.使用 Spring 进行远程处理和 Web 服务

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

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

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

  • Hessian :通过使用 Spring 的HessianProxyFactoryBeanHessianServiceExporter,您可以通过 Caucho 提供的基于 HTTP 的轻量级二进制协议透明地公开您的服务。

  • 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 作为协议的示例。

1.1. 使用 RMI 公开服务

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

1.1.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)。这意味着将使用匿名端口与服务进行通信。

1.1.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将其链接。

1.2. 使用 Hessian 通过 HTTP 远程调用服务

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

1.2.1. 为 Hessian 连接 DispatcherServlet

Hessian 通过 HTTP 进行通信,并通过使用自定义 servlet 进行通信。通过使用 Spring 的DispatcherServlet原理(请参阅[webmvc#mvc-servlet]),我们可以连接这样的 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 名称相匹配。

1.2.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 导出。

另外,您可以在根应用程序上下文中(例如,在WEB-INF/applicationContext.xml中)创建HessianServiceExporter,如以下示例所示:

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

在后一种情况下,您应该在web.xml中为此导出程序定义一个相应的 servlet,最终结果相同:导出程序 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>

1.2.3. 在 Client 端上链接服务

通过使用HessianProxyFactoryBean,我们可以在 Client 端链接服务。与 RMI 示例相同的原理适用。我们创建一个单独的 bean 工厂或应用程序上下文,并通过使用AccountService来 Management 帐户来提及SimpleObject所在的以下 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>

1.2.4. 将 HTTP 基本身份验证应用于通过 Hessian 公开的服务

Hessian 的优点之一是我们可以轻松地应用 HTTP 基本身份验证,因为这两种协议都是基于 HTTP 的。例如,可以通过使用web.xml安全功能来应用常规的 HTTP 服务器安全性机制。通常,您无需在此处使用每个用户的安全凭证。相反,您可以使用在HessianProxyFactoryBean级别定义的共享凭据(类似于 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 项目。

1.3. 使用 HTTP 调用程序公开服务

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

在幕后,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_ahttp://openjdk.java.net/jeps/290

1.3.1. 公开服务对象

为服务对象设置 HTTP 调用程序基础结构与使用 Hessian 进行设置的方法非常相似。由于 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>

关于黑森 State 的部分所述,此类导出程序定义通过DispatcherServlet实例的标准 Map 工具公开。

另外,您可以在根应用程序上下文中(例如,在'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>

1.3.2. 在 Client 端链接服务

同样,从 Client 端链接服务与使用 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>

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

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

1.4. Web Services

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

  • 使用 JAX-WS 公开 Web 服务

  • 使用 JAX-WS 访问 Web 服务

除了在 Spring Core 中对 JAX-WS 的库存支持之外,Spring 产品组合还具有SpringWeb Service,这是一种针对 Contract 优先,文档驱动的 Web 服务的解决方案。

1.4.1. 使用 JAX-WS 公开基于 Servlet 的 Web 服务

Spring 为 JAX-WS servlet 端点实现提供了一个方便的 Base Class:SpringBeanAutowiringSupport。为了公开AccountService,我们扩展 Spring 的SpringBeanAutowiringSupport类并在此处实现我们的业务逻辑,通常将调用委派给业务层。我们使用 Spring 的@Autowired注解来表达对 SpringManagement 的 bean 的依赖。以下示例显示了扩展SpringBeanAutowiringSupport的类:

/**
 * 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 服务教程。

1.4.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进行 Comments 驱动的注入也有效。以下示例显示了如何定义这些 bean:

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

}

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

作为 GlassFish 项目的一部分开发的 Oracle JAX-WS RI,将 Spring 支持作为其 JAX-WS Commons 项目的一部分。这允许将 JAX-WS 端点定义为 SpringManagement 的 bean,类似于previous section中讨论的独立模式,但这次是在 Servlet 环境中。

Note

这在 Java EE 5 环境中不可移植。它主要用于将 JAX-WS RI 嵌入为 Web 应用程序一部分的非 EE 环境,例如 Tomcat。

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

有关设置和使用方式的详细信息,请参见https://jax-ws-commons.java.net/spring/

1.4.4. 使用 JAX-WS 访问 Web 服务

Spring 提供了两个工厂 bean 来创建 JAX-WS Web 服务代理,即LocalJaxWsServiceFactoryBeanJaxWsPortProxyFactoryBean。前者只能返回一个 JAX-WS 服务类供我们使用。后者是完整版本,可以返回实现我们的业务服务接口的代理。在以下示例中,我们再次使用JaxWsPortProxyFactoryBeanAccountService端点创建代理:

<bean id="accountWebService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean">
    <property name="serviceInterface" value="example.AccountService"/> (1)
    <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>
  • (1) 其中serviceInterface是 Client 使用的我们的业务界面。

wsdlDocumentUrl是 WSDL 文件的 URL。 Spring 在启动时需要使用它来创建 JAX-WS 服务。 namespaceUri对应于.wsdl 文件中的targetNamespaceserviceName对应于.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 文档以获取有关这些需求的详细信息。

1.5. 通过 JMS 公开服务

您还可以通过使用 JMS 作为基础通信协议来透明地公开服务。 Spring 框架中的 JMS 远程支持非常基本。它在same thread上和同一非事务Session中发送和接收。结果,吞吐量取决于实现方式。请注意,这些单线程和非事务性约束仅适用于 Spring 的 JMS 远程支持。请参阅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>

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

}

1.5.2. Client 端配置

Client 端只需要创建一个 Client 端代理即可实现约定的接口(CheckingAccountService)。

以下示例定义了可以注入到其他 Client 端对象中的 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));
    }

}

1.6. AMQP

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

Note

远程接口未实现自动检测

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

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

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

1.7. 选择技术时的注意事项

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

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

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

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

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

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

1.8. REST 端点

Spring 框架提供了两种选择来调用 REST 端点:

  • Using RestTemplate:具有同步模板方法 API 的原始 Spring RESTClient 端。

  • WebClient:一种非阻塞的,Reactive 的替代方案,它支持同步和异步以及流方案。

Note

从 5.0 开始,无阻塞,响应式WebClient提供了RestTemplate的现代替代方案,并有效支持同步和异步以及流方案。 RestTemplate将在将来的版本中弃用,并且以后将不会添加主要的新功能。

1.8.1. 使用 RestTemplate

RestTemplate通过 HTTPClient 端库提供了更高级别的 API。它使在一行中轻松调用 REST 端点变得容易。它公开了以下几组重载方法:

表 1. RestTemplate 方法

Method groupDescription
getForObject通过 GET 检索表示形式。
getForEntity通过使用 GET 检索ResponseEntity(即状态,标题和正文)。
headForHeaders通过使用 HEAD 检索资源的所有 Headers。
postForLocation通过使用 POST 创建新资源,并从响应中返回LocationHeaders。
postForObject通过使用 POST 创建新资源,并从响应中返回表示形式。
postForEntity通过使用 POST 创建新资源,并从响应中返回表示形式。
put通过使用 PUT 创建或更新资源。
patchForObject通过使用 PATCH 更新资源,并从响应中返回表示形式。请注意,JDK HttpURLConnection不支持PATCH,但是 Apache HttpComponents 和其他支持。
delete使用 DELETE 删除指定 URI 处的资源。
optionsForAllow通过使用 ALLOW 检索资源的允许的 HTTP 方法。
exchange前述方法的通用性强(且不那么固执)版本,可在需要时提供额外的灵 Active。它接受RequestEntity(包括 HTTP 方法,URL,Headers 和正文作为 Importing),并返回ResponseEntity


这些方法允许使用ParameterizedTypeReference而不是Class来指定具有泛型的响应类型。
| execute |执行请求的最通用方法,完全控制通过回调接口进行的请求准备和响应提取。

Initialization

默认构造函数使用java.net.HttpURLConnection执行请求。您可以使用ClientHttpRequestFactory实现切换到其他 HTTP 库。内置支持以下内容:

  • Apache HttpComponents

  • Netty

  • OkHttp

例如,要切换到 Apache HttpComponents,可以使用以下命令:

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

每个ClientHttpRequestFactory都公开特定于基础 HTTPClient 端库的配置选项,例如用于凭证,连接池和其他详细信息。

Tip

请注意,访问表示错误的响应状态(例如 401)时,HTTP 请求的java.net实现可能引发异常。如果这是一个问题,请切换到另一个 HTTPClient 端库。

URIs

许多RestTemplate方法都接受 URI 模板和 URI 模板变量,它们可以作为String变量参数或Map<String,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);

请注意,URI 模板是自动编码的,如以下示例所示:

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

// Results in request to "http://example.com/hotel%20list"

您可以使用RestTemplateuriTemplateHandler属性来自定义 URI 的 encodings。或者,您可以准备java.net.URI并将其传递到接受URIRestTemplate方法之一。

有关使用和编码 URI 的更多详细信息,请参见URI Links

Headers

您可以使用exchange()方法来指定请求 Headers,如以下示例所示:

String uriTemplate = "http://example.com/hotels/{hotel}";
URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42);

RequestEntity<Void> requestEntity = RequestEntity.get(uri)
        .header(("MyRequestHeader", "MyValue")
        .build();

ResponseEntity<String> response = template.exchange(requestEntity, String.class);

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

您可以通过许多返回ResponseEntityRestTemplate方法变体来获取响应 Headers。

Body

HttpMessageConverter的帮助下,通过RestTemplate方法传递和返回的对象将与原始内容进行转换。

在 POST 上,Importing 对象被序列化到请求主体,如以下示例所示:

URI location = template.postForLocation("http://example.com/people", person);

您无需显式设置请求的 Content-TypeHeaders。在大多数情况下,您可以找到基于源Object类型的兼容消息转换器,并且所选消息转换器会相应地设置 Content Type。如有必要,可以使用exchange方法显式提供Content-Type请求 Headers,从而影响选择哪个消息转换器。

在 GET 上,响应的主体反序列化为输出Object,如以下示例所示:

Person person = restTemplate.getForObject("http://example.com/people/{id}", Person.class, 42);

不需要明确设置请求的AcceptHeaders。在大多数情况下,可以根据预期的响应类型找到兼容的消息转换器,这有助于填充AcceptHeaders。如有必要,可以使用exchange方法显式提供AcceptHeaders。

默认情况下,RestTemplate注册所有内置的message converters,具体取决于有助于确定存在哪些可选转换库的 Classpath 检查。您还可以将消息转换器设置为显式使用。

Message Conversion

与 Spring WebFlux 中的相同

spring-web模块包含HttpMessageConverter协定,用于通过InputStreamOutputStream读写 HTTP 请求和响应的正文。 HttpMessageConverter实例用于 Client 端(例如RestTemplate)和服务器端(例如 Spring MVC REST 控制器)。

框架中提供了主要媒体(MIME)类型的具体实现,默认情况下,它们在 Client 端的RestTemplate和服务器端的RequestMethodHandlerAdapter注册(请参见配置消息转换器)。

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

表 2. HttpMessageConverter 实现

MessageConverterDescription
StringHttpMessageConverter可以从 HTTP 请求和响应读取和写入String实例的HttpMessageConverter实现。默认情况下,此转换器支持所有文本媒体类型(text/*)并以Content-Typetext/plain写入。
FormHttpMessageConverter可以从 HTTP 请求和响应中读取和写入表单数据的HttpMessageConverter实现。默认情况下,此转换器读取和写入application/x-www-form-urlencoded媒体类型。从MultiValueMap<String, String>读取表格数据并将其写入。
ByteArrayHttpMessageConverter可以从 HTTP 请求和响应读取和写入字节数组的HttpMessageConverter实现。默认情况下,此转换器支持所有媒体类型(*/*)并以Content-Type application/octet-stream写入。您可以通过设置supportedMediaTypes属性并覆盖getContentType(byte[])来覆盖它。
MarshallingHttpMessageConverter可以使用org.springframework.oxm包中的 Spring 的MarshallerUnmarshaller抽象来读取和写入 XML 的HttpMessageConverter实现。该转换器需要使用MarshallerUnmarshaller才能使用。您可以通过构造函数或 bean 属性注入它们。默认情况下,此转换器支持text/xmlapplication/xml
MappingJackson2HttpMessageConverter可以使用 Jackson 的ObjectMapper读取和写入 JSON 的HttpMessageConverter实现。您可以根据需要使用 Jackson 提供的 Comments 来自定义 JSONMap。当您需要进一步控制时(对于需要为特定类型提供自定义 JSON 序列化器/反序列化器的情况),可以通过ObjectMapper属性注入自定义ObjectMapper。默认情况下,此转换器支持application/json
MappingJackson2XmlHttpMessageConverter可以使用Jackson XMLextensionsXmlMapper读写 XML 的HttpMessageConverter实现。您可以根据需要使用 JAXB 或 Jackson 提供的 Comments 来自定义 XMLMap。当您需要进一步控制时(对于需要为特定类型提供自定义 XML 序列化器/反序列化器的情况),可以通过ObjectMapper属性注入自定义XmlMapper。默认情况下,此转换器支持application/xml
SourceHttpMessageConverter可以从 HTTP 请求和响应中读取和写入javax.xml.transform.SourceHttpMessageConverter实现。仅支持DOMSourceSAXSourceStreamSource。默认情况下,此转换器支持text/xmlapplication/xml
BufferedImageHttpMessageConverter可以从 HTTP 请求和响应中读取和写入java.awt.image.BufferedImageHttpMessageConverter实现。该转换器读取和写入 Java I/O API 支持的媒体类型。
Jackson JSON 视图

您可以指定JacksonJSON 视图来仅序列化对象属性的一个子集,如以下示例所示:

MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);

RequestEntity<MappingJacksonValue> requestEntity =
    RequestEntity.post(new URI("http://example.com/user")).body(value);

ResponseEntity<String> response = template.exchange(requestEntity, String.class);
Multipart

要发送 Multipart 数据,您需要提供一个MultiValueMap<String, ?>,其值要么是代表 Component 内容的Object实例,要么是代表 Component 内容和头的HttpEntity实例。 MultipartBodyBuilder提供了一个方便的 API 来准备 Multipart 请求,如以下示例所示:

MultipartBodyBuilder builder = new MultipartBodyBuilder();
    builder.part("fieldPart", "fieldValue");
    builder.part("filePart", new FileSystemResource("...logo.png"));
    builder.part("jsonPart", new Person("Jason"));

    MultiValueMap<String, HttpEntity<?>> parts = builder.build();

在大多数情况下,您不必为每个部分指定Content-Type。Content Type 是根据要序列化的HttpMessageConverter自动确定的,对于Resource则根据文件 extensions 自动确定。如有必要,您可以通过重载的生成器part方法之一显式提供MediaType供每个 Component 使用。

MultiValueMap准备就绪后,您可以将其传递给RestTemplate,如以下示例所示:

MultipartBodyBuilder builder = ...;
    template.postForObject("http://example.com/upload", builder.build(), Void.class);

如果MultiValueMap包含至少一个非String值,该值也可以表示常规表单数据(即application/x-www-form-urlencoded),则无需将Content-Type设置为multipart/form-data。当您使用MultipartBodyBuilder来确保HttpEntity包装器时,总是如此。

1.8.2. 使用 AsyncRestTemplate(不建议使用)

AsyncRestTemplate已弃用。对于所有您可能考虑使用AsyncRestTemplate的用例,请改用WebClient

2.企业 JavaBeans(EJB)集成

作为轻量级容器,Spring 通常被认为是 EJB 的替代品。我们确实相信,对于许多(即使不是大多数)应用程序和用例,Spring 作为容器,结合其在事务,ORM 和 JDBC 访问领域的丰富支持功能,比通过 EJB 实现等效功能是更好的选择。容器和 EJB。

但是,请务必注意,使用 Spring 不会阻止您使用 EJB。实际上,Spring 使访问 EJB 以及在其中实现 EJB 和功能变得更加容易。另外,使用 Spring 访问 EJB 提供的服务可以使这些服务的实现稍后在本地 EJB,远程 EJB 或 POJO(普通旧 Java 对象)变体之间透明切换,而不必更改 Client 端代码。

在本章中,我们将研究 Spring 如何帮助您访问和实现 EJB。当访问 Stateless 会话 Bean(SLSB)时,Spring 提供了特殊的价值,因此我们从讨论这个主题开始。

2.1. 访问 EJB

本节介绍如何访问 EJB。

2.1.1. Concepts

要在本地或远程 Stateless 会话 Bean 上调用方法,Client 端代码通常必须执行 JNDI 查找以获取(本地或远程)EJB Home 对象,然后对该对象使用create方法调用以获取实际的(本地或远程)Bean。 )EJB 对象。然后在 EJB 上调用一种或多种方法。

为了避免重复的低级代码,许多 EJB 应用程序都使用服务定位器和业务委托模式。这些比在整个 Client 端代码中喷射 JNDI 查找要好,但是它们的常规实现有很多缺点:

  • 通常,使用 EJB 的代码取决于 Service Locator 或 Business Delegate 单例,使其难以测试。

  • 在不使用业务委托的情况下使用服务定位器模式的情况下,应用程序代码仍然最终必须在 EJB home 上调用create()方法并处理产生的异常。因此,它仍然与 EJB API 和 EJB 编程模型的复杂性联系在一起。

  • 实现业务委托模式通常会导致大量的代码重复,我们必须编写许多在 EJB 上调用相同方法的方法。

Spring 的方法是允许创建和使用代理对象(通常在 Spring 容器内配置),这些代理对象充当无代码的业务委托。除非您在此类代码中实际添加了实际价值,否则您无需在手动编码的业务委托中编写另一个 Service Locator,另一个 JNDI 查找或重复方法。

2.1.2. 访问本地 SLSB

假设我们有一个需要使用本地 EJB 的 Web 控制器。我们遵循最佳实践,并使用 EJB 业务方法接口模式,以便 EJB 的本地接口扩展了非 EJB 特定的业务方法接口。我们将此业务方法界面称为MyComponent。以下示例显示了这样的接口:

public interface MyComponent {
    ...
}

使用业务方法接口模式的主要原因之一是确保本地接口中的方法签名与 bean 实现类之间的同步是自动的。另一个原因是,如果有必要的话,以后可以使我们更轻松地切换到服务的 POJO(普通旧 Java 对象)实现。我们还需要实现本地 home 接口,并提供实现SessionBeanMyComponent业务方法接口的实现类。现在,将 Web 层控制器连接到 EJB 实现所需要做的唯一 Java 编码是在控制器上公开MyComponent类型的 setter 方法。这会将引用另存为控制器中的实例变量。以下示例显示了如何执行此操作:

private MyComponent myComponent;

public void setMyComponent(MyComponent myComponent) {
    this.myComponent = myComponent;
}

随后,我们可以在控制器中的任何业务方法中使用此实例变量。现在,假设我们从 Spring 容器中获取控制器对象,我们可以(在相同上下文中)配置LocalStatelessSessionProxyFactoryBean实例,它是 EJB 代理对象。我们配置代理,并使用以下配置条目设置控制器的myComponent属性:

<bean id="myComponent"
        class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/myBean"/>
    <property name="businessInterface" value="com.mycom.MyComponent"/>
</bean>

<bean id="myController" class="com.mycom.myController">
    <property name="myComponent" ref="myComponent"/>
</bean>

尽管没有强迫您使用 AOP 概念来欣赏结果,但仍要依靠 Spring AOP 框架在幕后进行大量工作。 myComponent bean 定义为 EJB 创建了一个代理,该代理实现了业务方法接口。 EJB 本地主目录在启动时被缓存,因此只有一个 JNDI 查找。每次调用 EJB 时,代理都会在本地 EJB 上调用classname方法,并在 EJB 上调用相应的业务方法。

myController bean 定义将控制器类的myComponent属性设置为 EJB 代理。

或者(最好在许多此类代理定义的情况下),考虑在 Spring 的“ jee”名称空间中使用<jee:local-slsb>配置元素。以下示例显示了如何执行此操作:

<jee:local-slsb id="myComponent" jndi-name="ejb/myBean"
        business-interface="com.mycom.MyComponent"/>

<bean id="myController" class="com.mycom.myController">
    <property name="myComponent" ref="myComponent"/>
</bean>

这种 EJB 访问机制极大地简化了应用程序代码。 Web 层代码(或其他 EJBClient 端代码)与 EJB 的使用无关。要用 POJO 或模拟对象或其他测试存根替换该 EJB 引用,我们可以更改myComponent bean 定义而无需更改任何 Java 代码。此外,作为应用程序的一部分,我们不必编写任何一行 JNDI 查找或其他 EJB 管道代码。

实际应用中的基准和经验表明,这种方法的性能开销(涉及目标 EJB 的反射调用)是最小的,并且在常规使用中是无法检测到的。请记住,无论如何我们都不希望对 EJB 进行细粒度的调用,因为与应用程序服务器中的 EJB 基础结构相关联的成本很高。

关于 JNDI 查找有一个警告。在 bean 容器中,此类通常最好用作单例(没有理由使其成为原型)。但是,如果该 bean 容器预先实例化了单例(与各种 XML ApplicationContext变体一样),则在 EJB 容器加载目标 EJB 之前加载 bean 容器时可能会出现问题。这是因为 JNDI 查找是在此类的init()方法中执行的,然后进行了缓存,但是 EJB 尚未绑定到目标位置。解决方案是不预先实例化该工厂对象,而是让它在首次使用时创建。在 XML 容器中,您可以使用lazy-init属性来控制它。

尽管大多数 Spring 用户都不感兴趣,但是那些使用 EJB 进行编程 AOP 的用户可能希望查看LocalSlsbInvokerInterceptor

2.1.3. 访问远程 SLSB

除了使用SimpleRemoteStatelessSessionProxyFactoryBean<jee:remote-slsb>配置元素外,访问远程 EJB 与访问本地 EJB 基本相同。当然,无论是否使用 Spring,远程调用语义都适用:调用另一台计算机上另一台 VM 中的对象上的方法时,有时在使用情况和故障处理方面必须区别对待。

与非 Spring 方法相比,Spring 的 EJBClient 端支持增加了另一个优势。通常,在本地或远程调用 EJB 之间轻松地来回切换 EJBClient 端代码是有问题的。这是因为远程接口方法必须声明它们抛出RemoteException,而 Client 端代码必须对此进行处理,而本地接口方法则不需要。通常需要修改为需要移至远程 EJB 的本地 EJB 编写的 Client 端代码,以添加对远程异常的处理,为需要移至本地 EJB 的远程 EJB 编写的 Client 端代码可以保持不变,但可以执行以下操作:许多不必要的远程异常处理,或进行修改以删除该代码。使用 Spring 远程 EJB 代理,您不能在业务方法接口和实现 EJB 代码中声明任何抛出的RemoteException,具有相同的远程接口(除了它确实抛出RemoteException),并且依靠代理来动态地处理两个接口就像它们一样。也就是说,Client 端代码不必处理已检查的RemoteException类。在 EJB 调用期间抛出的任何实际RemoteException都将重新抛出为未经检查的RemoteAccessException类,该类是RuntimeException的子类。然后,您可以在本地 EJB 或远程 EJB(甚至纯 Java 对象)实现之间随意切换目标服务,而无需了解或关心 Client 端代码。当然,这是可选的:没有什么可以阻止您在业务界面中声明RemoteException

2.1.4. 访问 EJB 2.x SLSB 与 EJB 3 SLSB

通过 Spring 访问 EJB 2.x 会话 Bean 和 EJB 3 会话 Bean 在很大程度上是透明的。 Spring 的 EJB 访问器(包括<jee:local-slsb><jee:remote-slsb>设施)在运行时透明地适应实际组件。它们会处理一个 Home 接口(如果找到)(EJB 2.x 样式),或者在没有可用 Home 接口(EJB 3 样式)的情况下执行直接组件调用。

注意:对于 EJB 3 会话 Bean,您还可以有效地使用JndiObjectFactoryBean/<jee:jndi-lookup>,因为公开了完全可用的组件引用以用于在那里的普通 JNDI 查找。定义明确的<jee:local-slsb><jee:remote-slsb>查找可提供一致且更明确的 EJB 访问配置。

3. JMS(Java 消息服务)

Spring 提供了一个 JMS 集成框架,该框架简化了 JMS API 的使用,就像 Spring 对 JDBC API 的集成一样。

JMS 可以大致分为两个功能区域,即消息的产生和使用。 JmsTemplate类用于消息生成和同步消息接收。对于类似于 Java EE 的消息驱动 bean 样式的异步接收,Spring 提供了许多消息侦听器容器,可用于创建消息驱动 POJO(MDP)。 Spring 还提供了一种声明式方法来创建消息侦听器。

org.springframework.jms.core软件包提供了使用 JMS 的核心功能。它包含 JMS 模板类,该类通过处理资源的创建和释放来简化 JMS 的使用,就像JdbcTemplate对于 JDBC 一样。 Spring 模板类共有的设计原则是提供帮助器方法来执行常用操作,并且对于更复杂的用法,将处理任务的本质委托给用户实现的回调接口。 JMS 模板遵循相同的设计。这些类提供了各种方便的方法,用于发送消息,同步使用消息以及向用户公开 JMS 会话和消息生成器。

org.springframework.jms.support软件包提供JMSException翻译功能。转换将已检查的JMSException层次结构转换为未检查的异常的镜像层次结构。如果存在选中的javax.jms.JMSException的任何提供程序特定的子类,则将此异常包装在未选中的UncategorizedJmsException中。

org.springframework.jms.support.converter包提供MessageConverter抽象以在 Java 对象和 JMS 消息之间进行转换。

org.springframework.jms.support.destination包提供了用于 ManagementJMS 目的地的各种策略,例如为 JNDI 中存储的目的地提供服务定位器。

org.springframework.jms.annotation软件包提供了必要的基础结构,以通过使用@JmsListener支持 Comments 驱动的侦听器端点。

org.springframework.jms.config软件包为jms名称空间提供了解析器实现,并提供了 Java config 支持以配置侦听器容器和创建侦听器端点。

最后,org.springframework.jms.connection软件包提供了适用于独立应用程序的ConnectionFactory的实现。它还包含用于 JMS 的 Spring PlatformTransactionManager的实现(巧妙地名为JmsTransactionManager)。这允许将 JMS 作为事务资源无缝集成到 Spring 的事务 Management 机制中。

3.1. 使用 Spring JMS

本节描述如何使用 Spring 的 JMS 组件。

3.1.1. 使用 JmsTemplate

JmsTemplate类是 JMS 核心软件包中的中心类。由于它在发送或同步接收消息时处理资源的创建和释放,因此它简化了 JMS 的使用。

使用JmsTemplate的代码仅需要实现回调接口,即可为其提供明确定义的高级 Contract。当给JmsTemplate中的调用代码提供Session时,MessageCreator回调接口会创建一条消息。为了允许更复杂地使用 JMS API,SessionCallback提供了 JMS 会话,而ProducerCallback公开了SessionMessageProducer对。

JMS API 公开了两种类型的发送方法,一种采用交付模式,优先级和生存时间作为服务质量(QOS)参数,另一种不采用 QOS 参数并使用默认值。由于JmsTemplate有许多发送方法,因此设置 QOS 参数已作为 bean 属性公开,以避免重复发送方法。同样,使用setReceiveTimeout属性设置同步接收调用的超时值。

某些 JMS 提供程序允许通过ConnectionFactory的配置来 Management 默认 QOS 值的设置。这样做的结果是,对MessageProducer实例的send方法(send(Destination destination, Message message))的调用使用与 JMS 规范中指定的 QOS 默认值不同的 QOS 默认值。为了提供对 QOS 值的一致 Management,因此,必须通过将布尔属性isExplicitQosEnabled设置为true来专门使JmsTemplate使用其自己的 QOS 值。

为方便起见,JmsTemplate还公开了一个基本的请求-答复操作,该操作允许发送消息并 await 作为该操作一部分而创建的临时队列的答复。

Tip

JmsTemplate类的实例一旦配置便是线程安全的。这很重要,因为这意味着您可以配置JmsTemplate的单个实例,然后将该共享引用安全地注入多个协作者中。需要明确的是,JmsTemplate是有状态的,因为它保持对ConnectionFactory的引用,但是此状态不是会话状态。

从 Spring Framework 4.1 开始,JmsMessagingTemplate构建在JmsTemplate的基础上,并提供了与消息传递抽象(即org.springframework.messaging.Message)的集成。这使您可以创建以通用方式发送的消息。

3.1.2. Connections

JmsTemplate要求引用ConnectionFactoryConnectionFactory是 JMS 规范的一部分,并且是使用 JMS 的入口点。Client 端应用程序使用它作为工厂来创建与 JMS 提供程序的连接,并封装各种配置参数,其中许多是特定于供应商的,例如 SSL 配置选项。

当在 EJB 中使用 JMS 时,供应商提供 JMS 接口的实现,以便它们可以参与声明式事务 Management 并执行连接和会话的池化。为了使用此实现,Java EE 容器通常要求您在 EJB 或 Servlet 部署 Descriptors 中将 JMS 连接工厂声明为resource-ref。为了确保在 EJB 内的JmsTemplate中使用这些功能,Client 端应用程序应确保引用了ConnectionFactory的托管实现。

缓存消息传递资源

标准 API 涉及创建许多中间对象。要发送消息,请执行以下“ API”遍历:

ConnectionFactory->Connection->Session->MessageProducer->send

ConnectionFactorySend操作之间,创建并销毁了三个中间对象。为了优化资源使用并提高性能,Spring 提供了ConnectionFactory的两种实现。

Using SingleConnectionFactory

Spring 提供了ConnectionFactory接口SingleConnectionFactory的实现,该接口在所有createConnection()调用中返回相同的Connection,而忽略对close()的调用。这对于测试和独立环境很有用,因此同一连接可用于可能跨越任意数量事务的多个JmsTemplate调用。 SingleConnectionFactory引用了通常来自 JNDI 的标准ConnectionFactory

Using CachingConnectionFactory

CachingConnectionFactory扩展了SingleConnectionFactory的功能,并添加了SessionMessageProducerMessageConsumer实例的缓存。初始缓存大小设置为1。您可以使用sessionCacheSize属性来增加缓存的会话数。请注意,由于根据会话的确认模式缓存会话,因此实际缓存的会话数大于该数量,因此,当sessionCacheSize设置为 one 时,最多可以有四个缓存的会话实例(每个确认模式一个)。 MessageProducerMessageConsumer实例被缓存在它们自己的会话中,并且在缓存时还考虑了生产者和使用者的唯一属性。 MessageProducers 将根据其目的地进行缓存。基于由目标,selectors,noLocal 传递标志和持久订阅名称(如果创建持久使用者)组成的键来缓存 MessageConsumers。

3.1.3. 目的地 Management

目标是ConnectionFactory实例,是可以在 JNDI 中存储和检索的 JMSManagement 的对象。在配置 Spring 应用程序上下文时,可以使用 JNDI JndiObjectFactoryBean factory 类或<jee:jndi-lookup>对对象对 JMS 目标的引用执行依赖项注入。但是,如果应用程序中有大量目标,或者 JMS 提供程序具有独特的高级目标 Management 功能,则此策略通常很麻烦。这种高级目标 Management 的示例包括动态目标的创建或对目标的分层名称空间的支持。 JmsTemplate将目标名称的解析委托给实现DestinationResolver接口的 JMS 目标对象。 DynamicDestinationResolverJmsTemplate使用的默认实现,并且可以解析动态目标。还提供JndiDestinationResolver充当 JNDI 中包含的目的地的服务定位器,并且可以选择退回到DynamicDestinationResolver中包含的行为。

通常,仅在运行时才知道 JMS 应用程序中使用的目的地,因此,在部署应用程序时无法通过 Management 方式创建。这通常是因为在交互的系统组件之间存在共享的应用程序逻辑,这些组件根据已知的命名约定在运行时创建目标。即使创建动态目标不属于 JMS 规范的一部分,但大多数供应商都提供了此功能。动态目标是使用用户定义的名称创建的,该名称将它们与临时目标区分开来,并且通常未在 JNDI 中注册。各个提供者之间用于创建动态目的地的 API 有所不同,因为与目的地关联的属性是特定于供应商的。但是,供应商有时会做出一个简单的实现选择,就是忽略 JMS 规范中的警告,并使用方法TopicSession createTopic(String topicName)QueueSession createQueue(String queueName)方法来创建具有默认目标属性的新目标。根据供应商的实现,DynamicDestinationResolver然后还可以创建一个物理目标,而不是仅解决一个物理目标。

布尔属性pubSubDomain用于在知道正在使用哪个 JMS 域的情况下配置JmsTemplate。默认情况下,此属性的值为 false,指示将使用点对点域Queues。此属性(由JmsTemplate使用)通过DestinationResolver接口的实现确定动态目标解析的行为。

您还可以通过属性defaultDestinationJmsTemplate配置为默认目标。默认目标是带有不引用特定目标的发送和接收操作。

3.1.4. 消息侦听器容器

在 EJB 世界中,JMS 消息最常见的用途之一是驱动消息驱动的 bean(MDB)。 Spring 提供了一种解决方案,以不将用户绑定到 EJB 容器的方式创建消息驱动的 POJO(MDP)。 (有关 Spring 对 MDP 支持的详细介绍,请参见异步接收:消息驱动的 POJO。)从 Spring Framework 4.1 开始,可以用@JmsListenerComments 端点方法,请参见Comments 驱动的侦听器端点以获取更多详细信息。

消息侦听器容器用于从 JMS 消息队列接收消息,并驱动注入到其中的MessageListener。侦听器容器负责消息接收的所有线程,并分派到侦听器中进行处理。消息侦听器容器是 MDP 与消息传递提供程序之间的中介,并负责注册接收消息,参与事务,资源获取和释放,异常转换等。这使您可以编写与接收消息(并可能对其进行响应)相关的(可能很复杂的)业务逻辑,并将样板 JMS 基础结构问题委托给框架。

Spring 附带了两个标准的 JMS 消息侦听器容器,每个容器都有其专门的功能集。

Using SimpleMessageListenerContainer

此消息侦听器容器是两种标准样式中的简单容器。它在启动时创建固定数量的 JMS 会话和使用者,使用标准 JMS MessageConsumer.setMessageListener()方法注册侦听器,并将其留给 JMS 提供者执行侦听器回调。此变体不允许动态适应运行时需求或参与外部 Management 的事务。在兼容性方面,它非常接近独立 JMS 规范的精神,但通常与 Java EE 的 JMS 限制不兼容。

Note

尽管SimpleMessageListenerContainer不允许参与外部 Management 的事务,但它支持本机 JMS 事务。要启用此功能,可以将sessionTransacted标志切换为true,或者在 XML 名称空间中将acknowledge属性设置为transacted。然后,从您的侦听器抛出的异常会导致回滚,并重新传递消息。或者,考虑使用CLIENT_ACKNOWLEDGE模式,该模式在出现异常的情况下也可以重新传送,但不使用事务处理的Session实例,因此在事务协议中不包括任何其他Session操作(例如发送响应消息)。

Tip

默认的AUTO_ACKNOWLEDGE模式不能提供适当的可靠性保证。当侦听器执行失败时(由于提供者在侦听器调用之后会自动确认每条消息,没有异常要传播到提供者),或者在侦听器容器关闭时(您可以通过设置acceptMessagesWhileStopping标志进行配置),消息可能会丢失。确保出于可靠性需求(例如,为了可靠的队列处理和持久的主题订阅)使用事务处理的会话。

Using DefaultMessageListenerContainer

大多数情况下使用此消息侦听器容器。与SimpleMessageListenerContainer相比,此容器变体允许动态适应运行时需求,并且能够参与外部 Management 的事务。配置为JtaTransactionManager时,每个接收到的消息都将注册到 XA 事务中。结果,处理可以利用 XA 事务语义。该侦听器容器在对 JMS 提供程序的低要求,高级功能(例如参与外部 Management 的事务)以及与 Java EE 环境的兼容性之间取得了良好的平衡。

您可以自定义容器的缓存级别。请注意,当未启用缓存时,将为每个消息接收创建一个新的连接和一个新的会话。将此内容与具有高负载的非持久订阅结合使用可能会导致消息丢失。在这种情况下,请确保使用适当的缓存级别。

当代理关闭时,此容器还具有可恢复的功能。默认情况下,简单的BackOff实现每五秒钟重试一次。您可以为更细粒度的恢复选项指定自定义BackOff实现。有关示例,请参见 api-spring-framework/util/backoff/ExponentialBackOff.html [+516+]。

Note

与其同级(SimpleMessageListenerContainer)一样,DefaultMessageListenerContainer支持本机 JMS 事务,并允许自定义确认模式。如果对您的方案可行,则强烈建议在外部 Management 的事务上使用此方法,也就是说,如果 JVM 死亡,您可以偶尔接收重复消息。业务逻辑中的自定义重复消息检测步骤可以解决这种情况,例如以业务实体存在检查或协议表检查的形式。任何这样的安排都比其他安排更为有效:用 XA 事务包装整个处理过程(通过将DefaultMessageListenerContainer配置为JtaTransactionManager)来覆盖 JMS 消息的接收以及消息侦听器中业务逻辑的执行(包括数据库操作等)。

Tip

默认的AUTO_ACKNOWLEDGE模式不能提供适当的可靠性保证。当侦听器执行失败时(由于提供者在侦听器调用之后会自动确认每条消息,没有异常要传播到提供者),或者在侦听器容器关闭时(您可以通过设置acceptMessagesWhileStopping标志进行配置),消息可能会丢失。确保出于可靠性需求(例如,为了可靠的队列处理和持久的主题订阅)使用事务处理的会话。

3.1.5. TransactionManagement

Spring 提供了一个JmsTransactionManager,用于 Management 单个 JMS ConnectionFactory的事务。如数据访问一章的事务 Management 部分所述,这使 JMS 应用程序可以利用 Spring 的托管事务功能。 JmsTransactionManager执行本地资源事务,将来自指定ConnectionFactory的 JMS 连接/会话对绑定到线程。 JmsTemplate自动检测此类 Transaction 资源并相应地对其进行操作。

在 Java EE 环境中,ConnectionFactory汇集了 Connection 和 Session 实例,因此可以有效地在事务之间重用这些资源。在独立环境中,使用 Spring 的SingleConnectionFactory会导致共享 JMS Connection,每个事务都有自己的独立Session。或者,考虑使用提供程序专用的池适配器,例如 ActiveMQ 的PooledConnectionFactory类。

您还可以将JmsTemplateJtaTransactionManager和具有 XA 功能的 JMS ConnectionFactory结合使用来执行分布式事务。请注意,这需要使用 JTA 事务 Management 器以及正确的 XA 配置的 ConnectionFactory。 (检查您的 Java EE 服务器或 JMS 提供程序的文档.)

使用 JMS API 从Connection创建Session时,在托管和非托管事务环境中重用代码可能会造成混淆。这是因为 JMS API 只有一个工厂方法来创建Session,并且它需要事务和确认模式的值。在托管环境中,设置这些值是环境的事务基础结构的责任,因此,供应商对 JMS Connection 的包装将忽略这些值。在非托管环境中使用JmsTemplate时,可以通过使用属性sessionTransactedsessionAcknowledgeMode来指定这些值。当您将PlatformTransactionManagerJmsTemplate一起使用时,始终为模板提供事务 JMS Session

3.2. 发送信息

JmsTemplate包含许多发送消息的便捷方法。发送方法使用javax.jms.Destination对象指定目标,其他方法通过在 JNDI 查找中使用String指定目标。不使用目标参数的send方法使用默认目标。

以下示例使用MessageCreator回调从提供的Session对象创建文本消息:

import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.Session;

import org.springframework.jms.core.MessageCreator;
import org.springframework.jms.core.JmsTemplate;

public class JmsQueueSender {

    private JmsTemplate jmsTemplate;
    private Queue queue;

    public void setConnectionFactory(ConnectionFactory cf) {
        this.jmsTemplate = new JmsTemplate(cf);
    }

    public void setQueue(Queue queue) {
        this.queue = queue;
    }

    public void simpleSend() {
        this.jmsTemplate.send(this.queue, new MessageCreator() {
            public Message createMessage(Session session) throws JMSException {
                return session.createTextMessage("hello queue world");
            }
        });
    }
}

在前面的示例中,JmsTemplate是通过将引用传递给ConnectionFactory来构造的。或者,提供零参数构造函数和connectionFactory,它们可用于以 JavaBean 样式(使用BeanFactory或纯 Java 代码)构造实例。或者,考虑从 Spring 的JmsGatewaySupport便捷 Base Class 派生,该 Base Class 为 JMS 配置提供了预构建的 bean 属性。

send(String destinationName, MessageCreator creator)方法使您可以使用目标的字符串名称发送消息。如果这些名称已在 JNDI 中注册,则应将模板的destinationResolver属性设置为JndiDestinationResolver的实例。

如果您创建了JmsTemplate并指定了默认目的地,则send(MessageCreator c)会向该目的地发送一条消息。

3.2.1. 使用消息转换器

为了方便域模型对象的发送,JmsTemplate具有各种发送方法,这些方法将 Java 对象作为消息数据内容的参数。 JmsTemplate中的重载方法convertAndSend()receiveAndConvert()方法将转换过程委托给MessageConverter接口的实例。该接口定义了一个简单的协定,可以在 Java 对象和 JMS 消息之间进行转换。默认实现(SimpleMessageConverter)支持StringTextMessagebyte[]BytesMesssage以及java.util.MapMapMessage之间的转换。通过使用转换器,您和您的应用程序代码可以专注于通过 JMS 发送或接收的业务对象,而不必担心如何将其表示为 JMS 消息。

沙箱当前包含一个MapMessageConverter,它使用反射在 JavaBean 和MapMessage之间进行转换。您可能自己实现的其他流行实现选择是使用现有 XML 编组程序包(例如 JAXB,Castor 或 XStream)创建代表对象的TextMessage的转换器。

为了适应消息属性,Headers 和正文的设置,这些属性通常不能封装在转换器类中,因此MessagePostProcessor接口使您可以在转换消息之后但在发送消息之前对其进行访问。以下示例显示了将java.util.Map转换为消息后如何修改消息头和属性:

public void sendWithConversion() {
    Map map = new HashMap();
    map.put("Name", "Mark");
    map.put("Age", new Integer(47));
    jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() {
        public Message postProcessMessage(Message message) throws JMSException {
            message.setIntProperty("AccountID", 1234);
            message.setJMSCorrelationID("123-00001");
            return message;
        }
    });
}

这将导致以下形式的消息:

MapMessage={
    Header={
        ... standard headers ...
        CorrelationID={123-00001}
    }
    Properties={
        AccountID={Integer:1234}
    }
    Fields={
        Name={String:Mark}
        Age={Integer:47}
    }
}

3.2.2. 使用 SessionCallback 和 ProducerCallback

尽管发送操作涵盖了许多常见的使用场景,但是您有时可能希望对 JMS SessionMessageProducer执行多个操作。 SessionCallbackProducerCallback分别暴露 JMS SessionSession/MessageProducer对。 JmsTemplate上的execute()方法执行这些回调方法。

3.3. 接收讯息

这描述了如何在 Spring 中使用 JMS 接收消息。

3.3.1. 同步接收

虽然 JMS 通常与异步处理相关联,但是您可以同步使用消息。重载的receive(..)方法提供了此功能。在同步接收期间,调用线程将阻塞,直到消息可用为止。这可能是危险的操作,因为调用线程可能会无限期地被阻塞。 receiveTimeout属性指定接收者在放弃 await 消息之前应该 await 多长时间。

3.3.2. 异步接收:消息驱动的 POJO

Note

Spring 还通过使用@JmsListenerComments 支持带 Comments 的侦听器端点,并提供了开放的基础结构以编程方式注册端点。到目前为止,这是设置异步接收器的最便捷方法。有关更多详细信息,请参见启用侦听器端点 Comments

消息驱动 POJO(MDP)以类似于 EJB 世界中的消息驱动 Bean(MDB)的方式充当 JMS 消息的接收者。 MDP 的一个限制(但请参见Using MessageListenerAdapter)是它必须实现javax.jms.MessageListener接口。请注意,如果您的 POJO 在多个线程上接收消息,则重要的是要确保您的实现是线程安全的。

以下示例显示了 MDP 的简单实现:

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

public class ExampleListener implements MessageListener {

    public void onMessage(Message message) {
        if (message instanceof TextMessage) {
            try {
                System.out.println(((TextMessage) message).getText());
            }
            catch (JMSException ex) {
                throw new RuntimeException(ex);
            }
        }
        else {
            throw new IllegalArgumentException("Message must be of type TextMessage");
        }
    }
}

实现MessageListener之后,就可以创建消息侦听器容器了。

以下示例显示如何定义和配置 Spring 附带的消息侦听器容器之一(在本例中为DefaultMessageListenerContainer):

<!-- this is the Message Driven POJO (MDP) -->
<bean id="messageListener" class="jmsexample.ExampleListener"/>

<!-- and this is the message listener container -->
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
    <property name="connectionFactory" ref="connectionFactory"/>
    <property name="destination" ref="destination"/>
    <property name="messageListener" ref="messageListener"/>
</bean>

有关每个实现所支持功能的完整说明,请参见各种消息侦听器容器(所有这些都实现MessageListenerContainer)的 Spring javadoc。

3.3.3. 使用 SessionAwareMessageListener 接口

SessionAwareMessageListener接口是特定于 Spring 的接口,它提供与 JMS MessageListener接口相似的协定,但还使消息处理方法可以访问从中接收Message的 JMS Session。以下 Lists 显示了SessionAwareMessageListener接口的定义:

package org.springframework.jms.listener;

public interface SessionAwareMessageListener {

    void onMessage(Message message, Session session) throws JMSException;
}

如果希望 MDP 能够响应任何接收到的消息(使用onMessage(Message, Session)方法中提供的Session),则可以选择让 MDP 实现此接口(优先于标准 JMS MessageListener接口)。 Spring 附带的所有消息侦听器容器实现都支持实现MessageListenerSessionAwareMessageListener接口的 MDP。实现SessionAwareMessageListener的类带有警告,然后通过接口将它们绑定到 Spring。是否使用它的选择完全由您作为应用程序开发人员或架构师来决定。

请注意,SessionAwareMessageListener接口的onMessage(..)方法抛出JMSException。与标准 JMS MessageListener接口相反,在使用SessionAwareMessageListener接口时,Client 端代码负责处理所有引发的异常。

3.3.4. 使用 MessageListenerAdapter

MessageListenerAdapter类是 Spring 异步消息传递支持中的最后一个组件。简而言之,它使您几乎可以将任何类公开为 MDP(尽管存在一些约束)。

考虑以下接口定义:

public interface MessageDelegate {

    void handleMessage(String message);

    void handleMessage(Map message);

    void handleMessage(byte[] message);

    void handleMessage(Serializable message);
}

请注意,尽管该接口既未扩展MessageListener也未扩展SessionAwareMessageListener接口,但仍可以通过使用MessageListenerAdapter类将其用作 MDP。还要注意如何根据各种Message类型的内容来强类型化各种消息处理方法,它们可以接收和处理。

现在考虑MessageDelegate接口的以下实现:

public class DefaultMessageDelegate implements MessageDelegate {
    // implementation elided for clarity...
}

特别要注意的是,MessageDelegate接口(DefaultMessageDelegate类)的先前实现完全没有 JMS 依赖性。这确实是一个 POJO,我们可以通过以下配置将其变成 MDP:

<!-- this is the Message Driven POJO (MDP) -->
<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
    <constructor-arg>
        <bean class="jmsexample.DefaultMessageDelegate"/>
    </constructor-arg>
</bean>

<!-- and this is the message listener container... -->
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
    <property name="connectionFactory" ref="connectionFactory"/>
    <property name="destination" ref="destination"/>
    <property name="messageListener" ref="messageListener"/>
</bean>

下一个示例显示另一个 MDP,它只能处理接收 JMS TextMessage消息。请注意,实际上是如何将消息处理方法称为receive(MessageListenerAdapter中的消息处理方法的名称默认为handleMessage),但是它是可配置的(如本节后面所述)。还请注意receive(..)方法是如何强类型 Importing 的,以便仅接收和响应 JMS TextMessage消息。以下 Lists 显示了TextMessageDelegate接口的定义:

public interface TextMessageDelegate {

    void receive(TextMessage message);
}

下面的 Lists 显示了实现TextMessageDelegate接口的类:

public class DefaultTextMessageDelegate implements TextMessageDelegate {
    // implementation elided for clarity...
}

话务员MessageListenerAdapter的配置如下:

<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
    <constructor-arg>
        <bean class="jmsexample.DefaultTextMessageDelegate"/>
    </constructor-arg>
    <property name="defaultListenerMethod" value="receive"/>
    <!-- we don't want automatic message context extraction -->
    <property name="messageConverter">
        <null/>
    </property>
</bean>

请注意,如果messageListener收到的类型不是TextMessage的 JMS Message,则会抛出IllegalStateException(随后将其吞咽)。 MessageListenerAdapter类的另一个功能是,如果处理程序方法返回非无效值,则自动发送回响应Message的功能。考虑以下接口和类:

public interface ResponsiveTextMessageDelegate {

    // notice the return type...
    String receive(TextMessage message);
}
public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate {
    // implementation elided for clarity...
}

如果将DefaultResponsiveTextMessageDelegateMessageListenerAdapter结合使用,则从'receive(..)'方法的执行返回的任何非 null 值都将(在默认配置中)转换为TextMessage。然后将结果TextMessage发送到原始Message的 JMS Reply-To属性或MessageListenerAdapter上设置的默认Destination(如果已配置)的 JMS Reply-To属性中定义的Destination(如果存在)。如果未找到Destination,则会引发InvalidDestinationException(请注意,该异常不会被吞没,并且会在调用堆栈中传播)。

3.3.5. 处理事务中的消息

在事务中调用消息侦听器仅需要重新配置侦听器容器。

您可以通过侦听器容器定义上的sessionTransacted标志激活本地资源事务。然后,每个消息侦听器调用都在活动的 JMS 事务中运行,并且在侦听器执行失败的情况下回退消息接收。 (通过SessionAwareMessageListener)发送响应消息是同一本地事务的一部分,但是任何其他资源操作(例如数据库访问)都是独立运行的。这通常需要在侦听器实现中进行重复消息检测,以解决数据库处理已提交但消息处理未能提交的情况。

考虑以下 bean 定义:

<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
    <property name="connectionFactory" ref="connectionFactory"/>
    <property name="destination" ref="destination"/>
    <property name="messageListener" ref="messageListener"/>
    <property name="sessionTransacted" value="true"/>
</bean>

要参与外部 Management 的事务,您需要配置一个事务 Management 器并使用支持外部 Management 的事务(通常为DefaultMessageListenerContainer)的侦听器容器。

要为 XA 事务参与配置消息侦听器容器,您需要配置JtaTransactionManager(默认情况下,它委派给 Java EE 服务器的事务子系统)。请注意,底层的 JMS ConnectionFactory必须具有 XA 功能,并已向您的 JTA 事务协调器正确注册。 (检查 Java EE 服务器的 JNDI 资源配置.)这使消息接收和(例如)数据库访问成为同一事务的一部分(具有统一的提交语义,但以 XA 事务日志开销为代价)。

以下 bean 定义创建一个事务 Management 器:

<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

然后,我们需要将其添加到我们之前的容器配置中。容器负责其余的工作。以下示例显示了如何执行此操作:

<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
    <property name="connectionFactory" ref="connectionFactory"/>
    <property name="destination" ref="destination"/>
    <property name="messageListener" ref="messageListener"/>
    <property name="transactionManager" ref="transactionManager"/> (1)
</bean>
  • (1) 我们的 TransactionManager。

3.4. 支持 JCA 消息端点

从 2.5 版开始,Spring 还提供了对基于 JCA 的MessageListener容器的支持。 JmsMessageEndpointManager尝试根据提供者的ResourceAdapter类名自动确定ActivationSpec类名。因此,通常可以提供 Spring 的通用JmsActivationSpecConfig,如以下示例所示:

<bean class="org.springframework.jms.listener.endpoint.JmsMessageEndpointManager">
    <property name="resourceAdapter" ref="resourceAdapter"/>
    <property name="activationSpecConfig">
        <bean class="org.springframework.jms.listener.endpoint.JmsActivationSpecConfig">
            <property name="destinationName" value="myQueue"/>
        </bean>
    </property>
    <property name="messageListener" ref="myMessageListener"/>
</bean>

或者,您可以使用给定的ActivationSpec对象设置JmsMessageEndpointManagerActivationSpec对象也可以来自 JNDI 查找(使用<jee:jndi-lookup>)。以下示例显示了如何执行此操作:

<bean class="org.springframework.jms.listener.endpoint.JmsMessageEndpointManager">
    <property name="resourceAdapter" ref="resourceAdapter"/>
    <property name="activationSpec">
        <bean class="org.apache.activemq.ra.ActiveMQActivationSpec">
            <property name="destination" value="myQueue"/>
            <property name="destinationType" value="javax.jms.Queue"/>
        </bean>
    </property>
    <property name="messageListener" ref="myMessageListener"/>
</bean>

使用 Spring 的ResourceAdapterFactoryBean,您可以在本地配置目标ResourceAdapter,如以下示例所示:

<bean id="resourceAdapter" class="org.springframework.jca.support.ResourceAdapterFactoryBean">
    <property name="resourceAdapter">
        <bean class="org.apache.activemq.ra.ActiveMQResourceAdapter">
            <property name="serverUrl" value="tcp://localhost:61616"/>
        </bean>
    </property>
    <property name="workManager">
        <bean class="org.springframework.jca.work.SimpleTaskWorkManager"/>
    </property>
</bean>

指定的WorkManager也可以指向特定于环境的线程池-通常通过SimpleTaskWorkManager实例的asyncTaskExecutor属性来指向。如果您碰巧使用多个适配器,请考虑为所有ResourceAdapter实例定义一个共享线程池。

在某些环境(例如 WebLogic 9 或更高版本)中,您可以(通过使用<jee:jndi-lookup>)从 JNDI 获取整个ResourceAdapter对象。然后,基于 Spring 的消息侦听器可以与服务器托管的ResourceAdapter进行交互,该服务器也使用服务器的内置WorkManager

有关更多详细信息,请参见JmsMessageEndpointManagerJmsActivationSpecConfigResourceAdapterFactoryBean的 javadoc。

Spring 还提供了与 JMS 无关的通用 JCA 消息端点 Management 器:org.springframework.jca.endpoint.GenericMessageEndpointManager。该组件允许使用任何消息侦听器类型(例如 CCI MessageListener)和任何特定于提供程序的ActivationSpec对象。请参阅 JCA 提供程序的文档以了解连接器的实际功能,并请参阅GenericMessageEndpointManager javadoc 以获取特定于 Spring 的配置详细信息。

Note

基于 JCA 的消息端点 Management 与 EJB 2.1 消息驱动 Bean 非常相似。它使用相同的基础资源提供者 Contract。与 EJB 2.1 MDB 一样,您也可以在 Spring 上下文中使用 JCA 提供程序支持的任何消息侦听器接口。尽管如此,Spring 仍为 JMS 提供了明确的“便利”支持,因为 JMS 是 JCA 端点 Management 协定中最常用的端点 API。

3.5. Comments 驱动的侦听器端点

异步接收消息的最简单方法是使用带 Comments 的侦听器端点基础结构。简而言之,它使您可以将托管 Bean 的方法公开为 JMS 侦听器端点。以下示例显示了如何使用它:

@Component
public class MyService {

    @JmsListener(destination = "myDestination")
    public void processOrder(String data) { ... }
}

前面示例的想法是,只要javax.jms.Destination myDestination上有消息可用,就相应地调用processOrder方法(在这种情况下,使用 JMS 消息的内容,类似于MessageListenerAdapter提供的内容)。

带 Comments 的终结点基础结构通过使用JmsListenerContainerFactory在幕后为每种带 Comments 的方法创建一个消息侦听器容器。此类容器未针对应用程序上下文进行注册,但可以通过使用JmsListenerEndpointRegistry bean 进行轻松定位以进行 Management。

Tip

@JmsListener是 Java 8 上的可重复 Comments,因此您可以通过向其添加其他@JmsListener声明来将多个 JMS 目标与同一方法相关联。

3.5.1. 启用侦听器端点 Comments

要启用对@JmsListenerComments 的支持,可以将@EnableJms添加到@Configuration类之一,如以下示例所示:

@Configuration
@EnableJms
public class AppConfig {

    @Bean
    public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        factory.setDestinationResolver(destinationResolver());
        factory.setSessionTransacted(true);
        factory.setConcurrency("3-10");
        return factory;
    }
}

默认情况下,基础结构将查找名为jmsListenerContainerFactory的 bean 作为工厂用来创建消息侦听器容器的源。在这种情况下(并忽略了 JMS 基础结构设置),您可以使用三个线程的核心轮询大小和十个线程的最大池大小来调用processOrder方法。

您可以自定义用于每个 Comments 的侦听器容器工厂,也可以通过实现JmsListenerConfigurer接口来配置显式默认值。仅当至少一个端点在没有特定容器工厂的情况下注册时,才需要使用默认值。有关详细信息和示例,请参见实现JmsListenerConfigurer的类的 javadoc。

如果您更喜欢XML configuration,则可以使用<jms:annotation-driven>元素,如以下示例所示:

<jms:annotation-driven/>

<bean id="jmsListenerContainerFactory"
        class="org.springframework.jms.config.DefaultJmsListenerContainerFactory">
    <property name="connectionFactory" ref="connectionFactory"/>
    <property name="destinationResolver" ref="destinationResolver"/>
    <property name="sessionTransacted" value="true"/>
    <property name="concurrency" value="3-10"/>
</bean>

3.5.2. 程序化端点注册

JmsListenerEndpoint提供 JMS 端点的模型,并负责为该模型配置容器。除了JmsListenerComments 检测到的端点外,该基础结构还允许您以编程方式配置端点。以下示例显示了如何执行此操作:

@Configuration
@EnableJms
public class AppConfig implements JmsListenerConfigurer {

    @Override
    public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
        SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
        endpoint.setId("myJmsEndpoint");
        endpoint.setDestination("anotherQueue");
        endpoint.setMessageListener(message -> {
            // processing
        });
        registrar.registerEndpoint(endpoint);
    }
}

在前面的示例中,我们使用了SimpleJmsListenerEndpoint,它提供了实际的MessageListener进行调用。但是,您也可以构建自己的端点变体来描述自定义调用机制。

请注意,您可以完全跳过@JmsListener的使用,而只能通过JmsListenerConfigurer以编程方式注册您的端点。

3.5.3. 带 Comments 的端点方法签名

到目前为止,我们已经在端点中注入了一个简单的String,但实际上它可以具有非常灵活的方法签名。在以下示例中,我们将其重写为使用自定义 Headers 注入Order

@Component
public class MyService {

    @JmsListener(destination = "myDestination")
    public void processOrder(Order order, @Header("order_type") String orderType) {
        ...
    }
}

您可以在 JMS 侦听器端点中注入的主要元素如下:

  • 原始的javax.jms.Message或其任何子类(前提是它与传入的消息类型匹配)。

  • javax.jms.Session用于对本机 JMS API 的可选访问(例如,用于发送自定义回复)。

  • org.springframework.messaging.Message代表传入的 JMS 消息。请注意,此消息同时包含自定义 Headers 和标准 Headers(由JmsHeaders定义)。

  • @Header-带 Comments 的方法参数,用于提取特定的 Headers 值,包括标准的 JMSHeaders。

  • 一个带有@HeadersComments 的参数,还必须可以将其分配给java.util.Map才能访问所有 Headers。

  • 不是受支持的类型(MessageSession)之一的非 Comments 元素被视为有效负载。您可以通过用@PayloadComments 参数来使其明确。您还可以通过添加额外的@Valid来启用验证。

注入 Spring 的Message抽象的能力特别有用,它可以受益于存储在特定于传输的消息中的所有信息,而无需依赖于特定于传输的 API。以下示例显示了如何执行此操作:

@JmsListener(destination = "myDestination")
public void processOrder(Message<Order> order) { ... }

DefaultMessageHandlerMethodFactory提供了方法参数的处理,您可以进一步对其进行自定义以支持其他方法参数。您也可以在那里自定义转换和验证支持。

例如,如果我们想在处理Order之前确保其有效,则可以使用@ValidComments 有效负载并配置必要的验证器,如以下示例所示:

@Configuration
@EnableJms
public class AppConfig implements JmsListenerConfigurer {

    @Override
    public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
        registrar.setMessageHandlerMethodFactory(myJmsHandlerMethodFactory());
    }

    @Bean
    public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
        DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
        factory.setValidator(myValidator());
        return factory;
    }
}

3.5.4. 反应 Management

MessageListenerAdapter中的现有支持已使您的方法具有非void返回类型。在这种情况下,调用的结果将封装在javax.jms.Message中,该javax.jms.Message要么在原始消息的JMSReplyTo头中指定的目标中发送,要么在侦听器上配置的默认目标中发送。现在,您可以使用消息传递抽象的@SendToComments 设置该默认目标。

假设我们的processOrder方法现在应该返回OrderStatus,我们可以将其编写为自动发送响应,如以下示例所示:

@JmsListener(destination = "myDestination")
@SendTo("status")
public OrderStatus processOrder(Order order) {
    // order processing
    return status;
}

Tip

如果您有几种带有@JmsListenerComments 的方法,则还可以将@SendToComments 放置在类级别以共享默认的答复目标。

如果需要以与传输无关的方式设置其他 Headers,则可以使用类似于以下方法来返回Message

@JmsListener(destination = "myDestination")
@SendTo("status")
public Message<OrderStatus> processOrder(Order order) {
    // order processing
    return MessageBuilder
            .withPayload(status)
            .setHeader("code", 1234)
            .build();
}

如果需要在运行时计算响应目标,则可以将响应封装在JmsResponse实例中,该实例还提供要在运行时使用的目标。我们可以如下重写前一个示例:

@JmsListener(destination = "myDestination")
public JmsResponse<Message<OrderStatus>> processOrder(Order order) {
    // order processing
    Message<OrderStatus> response = MessageBuilder
            .withPayload(status)
            .setHeader("code", 1234)
            .build();
    return JmsResponse.forQueue(response, "status");
}

最后,如果您需要为响应指定一些 QoS 值,例如优先级或生存时间,则可以相应地配置JmsListenerContainerFactory,如以下示例所示:

@Configuration
@EnableJms
public class AppConfig {

    @Bean
    public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        QosSettings replyQosSettings = new QosSettings();
        replyQosSettings.setPriority(2);
        replyQosSettings.setTimeToLive(10000);
        factory.setReplyQosSettings(replyQosSettings);
        return factory;
    }
}

3.6. JMS 命名空间支持

Spring 提供了一个 XML 名称空间来简化 JMS 配置。要使用 JMS 命名空间元素,您需要引用 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"
        xmlns:jms="http://www.springframework.org/schema/jms" (1)
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms.xsd">

    <!-- bean definitions here -->

</beans>
  • (1) 引用 JMS 模式。

命名空间由三个顶级元素组成:<annotation-driven/><listener-container/><jca-listener-container/><annotation-driven/>启用Comments 驱动的侦听器端点的使用。 <listener-container/><jca-listener-container/>定义共享侦听器容器配置,并且可以包含<listener/>子元素。以下示例显示了两个侦听器的基本配置:

<jms:listener-container>

    <jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/>

    <jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/>

</jms:listener-container>

前面的示例等效于创建两个不同的侦听器容器 bean 定义和两个不同的MessageListenerAdapter bean 定义,如Using MessageListenerAdapter所示。除了前面示例中显示的属性之外,listener元素还可以包含多个可选属性。下表描述了所有可用属性:

表 3. JMS<listener>元素的属性

AttributeDescription
id托管侦听器容器的 Bean 名称。如果未指定,则会自动生成一个 Bean 名称。
destination(必填)该侦听器的目标名称,通过DestinationResolver策略解析。
ref(必填)处理程序对象的 bean 名称。
method要调用的处理程序方法的名称。如果ref属性指向MessageListener或 Spring SessionAwareMessageListener,则可以省略此属性。
response-destination向其发送响应消息的默认响应目标的名称。如果请求消息中不包含JMSReplyTo字段,则应用此方法。此目标的类型由侦听器容器的response-destination-type属性确定。请注意,这仅适用于具有返回值的侦听器方法,为此,每个结果对象都将转换为响应消息。
subscription持久订阅的名称(如果有)。
selector此侦听器的可选消息 selectors。
concurrency要启动此侦听器的并发会话或使用者的数量。该值可以是表示最大数的简单数字(例如5),也可以是指示下限和上限的范围(例如3-5)。请注意,指定的最小值仅是一个提示,在运行时可能会被忽略。默认值为容器提供的值。

<listener-container/>元素还接受几个可选属性。这允许自定义各种策略(例如taskExecutordestinationResolver)以及基本的 JMS 设置和资源引用。通过使用这些属性,您可以定义高度自定义的侦听器容器,同时仍然受益于命名空间的便利性。

您可以通过指定要通过factory-id属性公开的 Bean 的id来自动将此类设置公开为JmsListenerContainerFactory,如以下示例所示:

<jms:listener-container connection-factory="myConnectionFactory"
        task-executor="myTaskExecutor"
        destination-resolver="myDestinationResolver"
        transaction-manager="myTransactionManager"
        concurrency="10">

    <jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/>

    <jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/>

</jms:listener-container>

下表描述了所有可用属性。有关各个属性的更多详细信息,请参见AbstractMessageListenerContainer的类级 javadoc 及其具体子类。 Javadoc 还讨论了事务选择和消息重新交付方案。

表 4. JMS<listener-container>元素的属性

AttributeDescription
container-type此侦听器容器的类型。可用选项为defaultsimpledefault102simple102(默认选项为default)。
container-class自定义侦听器容器实现类,作为完全限定的类名。根据container-type属性,默认值为 Spring 的标准DefaultMessageListenerContainerSimpleMessageListenerContainer
factory-id用指定的id公开此元素定义为JmsListenerContainerFactory的设置,以便可以将其与其他端点重用。
connection-factory对 JMS ConnectionFactory bean 的引用(默认 bean 名称为connectionFactory)。
task-executor对 JMS 侦听器调用程序的 Spring TaskExecutor的引用。
destination-resolver对解决 JMS Destination实例的DestinationResolver策略的引用。
message-converter对将 JMS 消息转换为侦听器方法参数的MessageConverter策略的引用。默认值为SimpleMessageConverter
error-handlerErrorHandler策略的引用,该策略用于处理MessageListener执行期间可能发生的任何未捕获的异常。
destination-type此侦听器的 JMS 目标类型:queuetopicdurableTopicsharedTopicsharedDurableTopic。这可能会启用容器的pubSubDomainsubscriptionDurablesubscriptionShared属性。默认值为queue(将禁用这三个属性)。
response-destination-type响应的 JMS 目标类型:queuetopic。默认值为destination-type属性的值。
client-id此侦听器容器的 JMSClient 端 ID。使用持久订阅时必须指定它。
cacheJMS 资源的缓存级别:noneconnectionsessionconsumerauto。默认情况下(auto),缓存级别实际上是consumer,除非已指定外部事务 Management 器。在这种情况下,有效的默认值为none(假设 Java EE 风格的事务 Management,其中给定的 ConnectionFactory 是 XA 感知的池)。
acknowledge本机 JMS 确认模式:autoclientdups-oktransacted。值transacted激活本地处理的Session。或者,您可以指定transaction-manager属性,如表中稍后所述。默认值为auto
transaction-manager对外部PlatformTransactionManager的引用(通常是基于 XA 的事务协调器,例如 Spring 的JtaTransactionManager)。如果未指定,则使用本机确认(请参见acknowledge属性)。
concurrency每个侦听器启动的并发会话或使用者的数量。它可以是表示最大数的简单数字(例如5),也可以是指示下限和上限的范围(例如3-5)。请注意,指定的最小值只是一个提示,在运行时可能会被忽略。默认值为1。如果是主题侦听器或队列 Sequences 很重要,则应将并发限制为1。考虑将其提高到一般队列。
prefetch加载到单个会话中的最大消息数。请注意,增加此数字可能会导致并发 Consumer 饥饿。
receive-timeout用于接听电话的超时时间(以毫秒为单位)。默认值为1000(一秒)。 -1表示没有超时。
back-off指定用于计算两次恢复尝试间隔的BackOff实例。如果BackOffExecution实现返回BackOffExecution#STOP,则侦听器容器不会进一步尝试恢复。设置此属性时,将忽略recovery-interval值。默认值为FixedBackOff,间隔为 5000 毫秒(即五秒)。
recovery-interval指定两次恢复尝试之间的时间间隔(以毫秒为单位)。它提供了一种方便的方法来创建具有指定间隔的FixedBackOff。有关更多恢复选项,请考虑改为指定BackOff实例。缺省值为 5000 毫秒(即 5 秒)。
phase此容器应在其中启动和停止的生命周期阶段。值越低,此容器启动的越早,而容器停止的越晚。默认值为Integer.MAX_VALUE,这意味着容器将尽可能晚地启动,并尽快停止。

配置具有jms模式支持的基于 JCA 的侦听器容器非常相似,如以下示例所示:

<jms:jca-listener-container resource-adapter="myResourceAdapter"
        destination-resolver="myDestinationResolver"
        transaction-manager="myTransactionManager"
        concurrency="10">

    <jms:listener destination="queue.orders" ref="myMessageListener"/>

</jms:jca-listener-container>

下表描述了 JCA 变体的可用配置选项:

表 5. JMS<jca-listener-container/>元素的属性

AttributeDescription
factory-id用指定的id公开此元素定义为JmsListenerContainerFactory的设置,以便可以将其与其他端点重用。
resource-adapter对 JCA ResourceAdapter bean 的引用(默认 bean 名称为resourceAdapter)。
activation-spec-factoryJmsActivationSpecFactory的引用。默认设置是自动检测 JMS 提供程序及其ActivationSpec类(请参见DefaultJmsActivationSpecFactory)。
destination-resolver对解决 JMS DestinationsDestinationResolver策略的引用。
message-converter对将 JMS 消息转换为侦听器方法参数的MessageConverter策略的引用。默认值为SimpleMessageConverter
destination-type此侦听器的 JMS 目标类型:queuetopicdurableTopicsharedTopic。或sharedDurableTopic。这可能会启用容器的pubSubDomainsubscriptionDurablesubscriptionShared属性。默认值为queue(将禁用这三个属性)。
response-destination-type响应的 JMS 目标类型:queuetopic。默认值为destination-type属性的值。
client-id此侦听器容器的 JMSClient 端 ID。使用持久订阅时需要指定它。
acknowledge本机 JMS 确认模式:autoclientdups-oktransacted。值transacted激活本地处理的Session。或者,您可以指定后面描述的transaction-manager属性。默认值为auto
transaction-manager对 Spring JtaTransactionManagerjavax.transaction.TransactionManager的引用,用于为每个传入消息启动 XA 事务。如果未指定,则使用本机确认(请参见acknowledge属性)。
concurrency每个侦听器启动的并发会话或使用者的数量。它可以是表示最大数的简单数字(例如5),也可以是指示下限和上限的范围(例如3-5)。请注意,指定的最小值只是一个提示,通常在运行时使用 JCA 侦听器容器时将被忽略。预设值为 1.
prefetch加载到单个会话中的最大消息数。请注意,增加此数字可能会导致并发 Consumer 饥饿。

4. JMX

Spring 中的 JMX(JavaManagement 扩展)支持提供的功能使您可以轻松,透明地将 Spring 应用程序集成到 JMX 基础结构中。

JMX?

本章不是 JMX 的介绍。它没有试图解释为什么您可能要使用 JMX。如果您不熟悉 JMX,请参阅本章末尾的Further Resources

具体来说,Spring 的 JMX 支持提供了四个核心功能:

  • 将任何 Spring bean 自动注册为 JMX MBean。

  • 一种用于控制 beanManagement 界面的灵活机制。

  • 通过远程 JSR-160 连接器以声明方式公开 MBean。

  • 本地和远程 MBean 资源的简单代理。

这些功能旨在在不将应用程序组件耦合到 Spring 或 JMX 接口和类的情况下起作用。实际上,在大多数情况下,您的应用程序类无需了解 Spring 或 JMX 即可利用 Spring JMX 功能。

4.1. 将您的 Bean 导出到 JMX

Spring 的 JMX 框架的核心类是MBeanExporter。此类负责获取您的 Spring bean 并向 JMX MBeanServer注册它们。例如,考虑以下类:

package org.springframework.jmx;

public class JmxTestBean implements IJmxTestBean {

    private String name;
    private int age;
    private boolean isSuperman;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

    public String getName() {
        return name;
    }

    public int add(int x, int y) {
        return x + y;
    }

    public void dontExposeMe() {
        throw new RuntimeException();
    }
}

要将此 Bean 的属性和方法公开为 MBean 的属性和操作,可以在配置文件中配置MBeanExporter类的实例并传入 Bean,如以下示例所示:

<beans>
    <!-- this bean must not be lazily initialized if the exporting is to happen -->
    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter" lazy-init="false">
        <property name="beans">
            <map>
                <entry key="bean:name=testBean1" value-ref="testBean"/>
            </map>
        </property>
    </bean>
    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>
</beans>

前面的配置片段中相关的 bean 定义是exporter bean。 beans属性准确地告诉MBeanExporter您必须将哪个 bean 导出到 JMX MBeanServer。在默认配置中,beans Map中每个条目的键用作相应条目值所引用的 Bean 的ObjectName。您可以按照控制您的 Bean 的 ObjectName 实例中所述更改此行为。

使用此配置,testBean bean 在ObjectName bean:name=testBean1下作为 MBean 公开。默认情况下,bean 的所有public属性都作为属性公开,所有public方法(从Object类继承的方法除外)都作为操作公开。

Note

MBeanExporterLifecycle bean(请参阅启动和关机回调)。默认情况下,MBean 在应用程序生命周期中尽可能晚地导出。您可以通过设置autoStartup标志来配置导出发生的phase或禁用自动注册。

4.1.1. 创建一个 MBeanServer

preceding section中显示的配置假定该应用程序正在一个(并且只有一个)MBeanServer已运行的环境中运行。在这种情况下,Spring 尝试找到正在运行的MBeanServer并将您的 bean 注册到该服务器(如果有)。当您的应用程序在具有自己的MBeanServer的容器(例如 Tomcat 或 IBM WebSphere)中运行时,此行为很有用。

但是,此方法在独立环境中或在不提供MBeanServer的容器中运行时无用。为了解决这个问题,您可以通过在配置中添加org.springframework.jmx.support.MBeanServerFactoryBean类的实例来声明性地创建MBeanServer实例。您还可以通过将MBeanExporter实例的server属性的值设置为MBeanServerFactoryBean返回的MBeanServer值来确保使用特定的MBeanServer,如以下示例所示:

<beans>

    <bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean"/>

    <!--
    this bean needs to be eagerly pre-instantiated in order for the exporting to occur;
    this means that it must not be marked as lazily initialized
    -->
    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="bean:name=testBean1" value-ref="testBean"/>
            </map>
        </property>
        <property name="server" ref="mbeanServer"/>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

</beans>

在前面的示例中,MBeanServer的实例由MBeanServerFactoryBean创建,并通过server属性提供给MBeanExporter。当您提供自己的MBeanServer实例时,MBeanExporter不会尝试查找正在运行的MBeanServer并使用提供的MBeanServer实例。为了使其正常工作,您必须在 Classpath 上具有 JMX 实现。

4.1.2. 重用现有的 MBeanServer

如果未指定服务器,则MBeanExporter尝试自动检测正在运行的MBeanServer。在大多数仅使用一个MBeanServer实例的环境中,这是可行的。但是,当存在多个实例时,导出器可能选择了错误的服务器。在这种情况下,应使用MBeanServer agentId指示要使用的实例,如以下示例所示:

<beans>
    <bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
        <!-- indicate to first look for a server -->
        <property name="locateExistingServerIfPossible" value="true"/>
        <!-- search for the MBeanServer instance with the given agentId -->
        <property name="agentId" value="MBeanServer_instance_agentId>"/>
    </bean>
    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="server" ref="mbeanServer"/>
        ...
    </bean>
</beans>

对于平台或现有MBeanServer具有通过查找方法检索到的动态(或未知)agentId的情况,应使用factory-method,如以下示例所示:

<beans>
    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="server">
            <!-- Custom MBeanServerLocator -->
            <bean class="platform.package.MBeanServerLocator" factory-method="locateMBeanServer"/>
        </property>
    </bean>

    <!-- other beans here -->

</beans>

4.1.3. 延迟初始化的 MBean

如果为 Bean 配置了MBeanExporter,并且也为延迟初始化配置了该MBeanExporter,则MBeanExporter不会破坏该协定,并且避免实例化该 Bean。相反,它向MBeanServer注册了一个代理,并推迟从容器获取 Bean,直到对该代理进行第一次调用为止。

4.1.4. 自动注册 MBean

通过MBeanExporter导出并且已经是有效 MBean 的所有 bean 都将直接在MBeanServer上注册,而无需 Spring 的进一步干预。通过将autodetect属性设置为true,可以使MBeanExporter自动检测 MBean,如以下示例所示:

<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
    <property name="autodetect" value="true"/>
</bean>

<bean name="spring:mbean=true" class="org.springframework.jmx.export.TestDynamicMBean"/>

在前面的示例中,名为spring:mbean=true的 bean 已经是有效的 JMX MBean,并由 Spring 自动注册。缺省情况下,自动检测到 JMX 注册的 bean 的 Bean 名称用作ObjectName。您可以覆盖此行为,如控制您的 Bean 的 ObjectName 实例中所述。

4.1.5. 控制注册行为

考虑以下情形:Spring MBeanExporter尝试通过使用ObjectName bean:name=testBean1MBeanServer注册MBean。如果MBean实例已经在同一ObjectName下注册,则默认行为是失败(并抛出InstanceAlreadyExistsException)。

您可以精确控制将MBean注册到MBeanServer时发生的情况。当注册过程发现MBean已经在同一ObjectName下注册时,Spring 的 JMX 支持允许三种不同的注册行为来控制注册行为。下表总结了这些注册行为:

表 6.注册行为

Registration behaviorExplanation
FAIL_ON_EXISTING这是默认的注册行为。如果MBean实例已经在同一ObjectName下注册,则不注册正在注册的MBean,并抛出InstanceAlreadyExistsException。现有的MBean不受影响。
IGNORE_EXISTING如果MBean实例已经在同一ObjectName下注册,则正在注册的MBean不会被注册。现有的MBean不受影响,并且不会抛出Exception。这在多个应用程序要在共享MBeanServer中共享一个公共MBean的设置中很有用。
REPLACE_EXISTING如果MBean实例已经在同一ObjectName下注册,那么先前已注册的现有MBean将被取消注册,而新的MBean会在其位置注册(新的MBean有效地替换先前的实例)。

上表中的值定义为RegistrationPolicy类上的枚举。如果要更改默认注册行为,则需要将MBeanExporter定义上的registrationPolicy属性的值设置为这些值之一。

下面的示例显示如何从默认注册行为更改为REPLACE_EXISTING行为:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="bean:name=testBean1" value-ref="testBean"/>
            </map>
        </property>
        <property name="registrationPolicy" value="REPLACE_EXISTING"/>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

</beans>

4.2. 控制 Bean 的 Management 接口

preceding section的示例中,您几乎无法控制 bean 的 Management 界面。每个导出 bean 的所有public属性和方法分别作为 JMX 属性和操作公开。为了对已导出的 bean 的哪些属性和方法实际上作为 JMX 属性和操作公开而进行更细粒度的控制,Spring JMX 提供了一种全面且可扩展的机制来控制 bean 的 Management 接口。

4.2.1. 使用 MBeanInfoAssembler 接口

在幕后,MBeanExporter代表org.springframework.jmx.export.assembler.MBeanInfoAssembler接口的实现,该接口负责定义每个公开的 bean 的 Management 接口。默认实现org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler定义了一个 Management 接口,该接口公开了所有公共属性和方法(如您在前面几节的示例中所看到的)。 Spring 提供了MBeanInfoAssembler接口的两个附加实现,使您可以使用源级元数据或任何任意接口来控制生成的 Management 接口。

4.2.2. 使用源级元数据:JavaComments

通过使用MetadataMBeanInfoAssembler,您可以通过使用源级元数据来定义 bean 的 Management 接口。元数据的读取由org.springframework.jmx.export.metadata.JmxAttributeSource接口封装。 Spring JMX 提供了一个使用 JavaComments 的默认实现,即org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource。您必须为MetadataMBeanInfoAssembler配置JmxAttributeSource接口的实现实例,才能使其正常运行(没有默认值)。

要标记要导出到 JMX 的 bean,应使用ManagedResourceComments 对 bean 类进行 Comments。您必须使用_注解标记要公开的每个方法,并使用ManagedAttribute注解标记希望公开的每个属性。标记属性时,可以省略 getter 或 setter 的 Comments,以分别创建只写或只读属性。

Note

带有ManagedResourceComments 的 Bean 必须是公共的,公开操作或属性的方法也必须是公共的。

以下示例显示了我们在创建一个 MBeanServer中使用的JmxTestBean类的带 Comments 版本:

package org.springframework.jmx;

import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedAttribute;

@ManagedResource(
        objectName="bean:name=testBean4",
        description="My Managed Bean",
        log=true,
        logFile="jmx.log",
        currencyTimeLimit=15,
        persistPolicy="OnUpdate",
        persistPeriod=200,
        persistLocation="foo",
        persistName="bar")
public class AnnotationTestBean implements IJmxTestBean {

    private String name;
    private int age;

    @ManagedAttribute(description="The Age Attribute", currencyTimeLimit=15)
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @ManagedAttribute(description="The Name Attribute",
            currencyTimeLimit=20,
            defaultValue="bar",
            persistPolicy="OnUpdate")
    public void setName(String name) {
        this.name = name;
    }

    @ManagedAttribute(defaultValue="foo", persistPeriod=300)
    public String getName() {
        return name;
    }

    @ManagedOperation(description="Add two numbers")
    @ManagedOperationParameters({
        @ManagedOperationParameter(name = "x", description = "The first number"),
        @ManagedOperationParameter(name = "y", description = "The second number")})
    public int add(int x, int y) {
        return x + y;
    }

    public void dontExposeMe() {
        throw new RuntimeException();
    }

}

在前面的示例中,您可以看到JmxTestBean类标记有ManagedResourceComments,并且此ManagedResourceComments 配置了一组属性。这些属性可用于配置MBeanExporter生成的 MBean 的各个方面,稍后将在源级元数据类型中进行详细说明。

agename属性都带有ManagedAttributeComments,但是,在age属性的情况下,仅标记了吸气剂。这导致这两个属性都作为属性包含在 Management 界面中,但是age属性是只读的。

最后,add(int, int)方法带有ManagedOperation属性标记,而dontExposeMe()方法则没有。当您使用MetadataMBeanInfoAssembler时,这将导致 Management 界面仅包含一个操作(add(int, int))。

以下配置显示了如何配置MBeanExporter以使用MetadataMBeanInfoAssembler

<beans>
    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="assembler" ref="assembler"/>
        <property name="namingStrategy" ref="namingStrategy"/>
        <property name="autodetect" value="true"/>
    </bean>

    <bean id="jmxAttributeSource"
            class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>

    <!-- will create management interface using annotation metadata -->
    <bean id="assembler"
            class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
        <property name="attributeSource" ref="jmxAttributeSource"/>
    </bean>

    <!-- will pick up the ObjectName from the annotation -->
    <bean id="namingStrategy"
            class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
        <property name="attributeSource" ref="jmxAttributeSource"/>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.AnnotationTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>
</beans>

在前面的示例中,已为MetadataMBeanInfoAssembler bean 配置了AnnotationJmxAttributeSource类的实例,并通过汇编程序属性将其传递给MBeanExporter。这是为 Spring 公开的 MBean 利用元数据驱动的 Management 接口所需要的全部。

4.2.3. 源级元数据类型

下表描述了可在 Spring JMX 中使用的源级别元数据类型:

表 7.源级元数据类型

PurposeAnnotationAnnotation Type
Class的所有实例标记为 JMX 托管资源。@ManagedResourceClass
将方法标记为 JMX 操作。@ManagedOperationMethod
将一个 getter 或 setter 标记为 JMX 属性的一半。@ManagedAttribute方法(仅 getter 和 setter)
定义操作参数的描述。@ManagedOperationParameter@ManagedOperationParametersMethod

下表描述了可在这些源级元数据类型上使用的配置参数:

表 8.源级元数据参数

ParameterDescriptionApplies to
ObjectNameMetadataNamingStrategy用于确定托管资源的ObjectNameManagedResource
description设置资源,属性或操作的友好描述。ManagedResourceManagedAttributeManagedOperationManagedOperationParameter
currencyTimeLimit设置currencyTimeLimitDescriptors 字段的值。ManagedResourceManagedAttribute
defaultValue设置defaultValueDescriptors 字段的值。ManagedAttribute
log设置logDescriptors 字段的值。ManagedResource
logFile设置logFileDescriptors 字段的值。ManagedResource
persistPolicy设置persistPolicyDescriptors 字段的值。ManagedResource
persistPeriod设置persistPeriodDescriptors 字段的值。ManagedResource
persistLocation设置persistLocationDescriptors 字段的值。ManagedResource
persistName设置persistNameDescriptors 字段的值。ManagedResource
name设置操作参数的显示名称。ManagedOperationParameter
index设置操作参数的索引。ManagedOperationParameter

4.2.4. 使用 AutodetectCapableMBeanInfoAssembler 接口

为了进一步简化配置,Spring 包含了AutodetectCapableMBeanInfoAssembler接口,该接口扩展了MBeanInfoAssembler接口以添加对自动检测 MBean 资源的支持。如果用AutodetectCapableMBeanInfoAssembler的实例配置MBeanExporter,则可以在包含 Bean 的情况下“投票”,以暴露给 JMX。

AutodetectCapableMBeanInfo接口的唯一实现是MetadataMBeanInfoAssembler,该投票表决以包括所有带有ManagedResource属性标记的 bean。在这种情况下,默认方法是将 Bean 名称用作ObjectName,这将导致与以下内容类似的配置:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <!-- notice how no 'beans' are explicitly configured here -->
        <property name="autodetect" value="true"/>
        <property name="assembler" ref="assembler"/>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

    <bean id="assembler" class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
        <property name="attributeSource">
            <bean class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>
        </property>
    </bean>

</beans>

请注意,在上述配置中,没有将任何 bean 传递给MBeanExporter。但是,JmxTestBean仍被注册,因为它已被标记为ManagedResource属性,并且MetadataMBeanInfoAssembler检测到该问题并对其进行投票以将其包括在内。这种方法的唯一问题是JmxTestBean的名称现在具有商业意义。您可以通过更改控制您的 Bean 的 ObjectName 实例中定义的ObjectName创建的默认行为来解决此问题。

4.2.5. 使用 Java 接口定义 Management 接口

除了MetadataMBeanInfoAssembler之外,Spring 还包括InterfaceBasedMBeanInfoAssembler,它使您可以基于一组接口中定义的一组方法来约束公开的方法和属性。

尽管公开 MBean 的标准机制是使用接口和简单的命名方案,但是InterfaceBasedMBeanInfoAssembler扩展了此功能,它消除了对命名约定的需要,使您可以使用多个接口,并且不再需要 bean 来实现 MBean 接口。

考虑以下接口,该接口用于为我们先前显示的JmxTestBean类定义 Management 接口:

public interface IJmxTestBean {

    public int add(int x, int y);

    public long myOperation();

    public int getAge();

    public void setAge(int age);

    public void setName(String name);

    public String getName();

}

该接口定义在 JMX MBean 上作为操作和属性公开的方法和属性。以下代码显示了如何配置 Spring JMX 以使用此接口作为 Management 接口的定义:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="bean:name=testBean5" value-ref="testBean"/>
            </map>
        </property>
        <property name="assembler">
            <bean class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler">
                <property name="managedInterfaces">
                    <value>org.springframework.jmx.IJmxTestBean</value>
                </property>
            </bean>
        </property>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

</beans>

在前面的示例中,在为任何 bean 构造 Management 接口时,将InterfaceBasedMBeanInfoAssembler配置为使用IJmxTestBean接口。理解由InterfaceBasedMBeanInfoAssembler处理的 bean 不需要实现用于生成 JMXManagement 接口的接口,这一点很重要。

在上述情况下,IJmxTestBean接口用于为所有 bean 构造所有 Management 接口。在许多情况下,这不是理想的行为,并且您可能希望对不同的 bean 使用不同的接口。在这种情况下,您可以通过interfaceMappings属性传递InterfaceBasedMBeanInfoAssembler Properties实例,其中每个条目的键是 Bean 名称,每个条目的值是一个用逗号分隔的接口名称列表,用于该 Bean。

如果没有通过managedInterfacesinterfaceMappings属性指定 Management 接口,则InterfaceBasedMBeanInfoAssembler会在 Bean 上进行反映,并使用该 Bean 所实现的所有接口来创建 Management 接口。

4.2.6. 使用 MethodNameBasedMBeanInfoAssembler

MethodNameBasedMBeanInfoAssembler使您可以指定作为属性和操作公开给 JMX 的方法名称的列表。以下代码显示了示例配置:

<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
    <property name="beans">
        <map>
            <entry key="bean:name=testBean5" value-ref="testBean"/>
        </map>
    </property>
    <property name="assembler">
        <bean class="org.springframework.jmx.export.assembler.MethodNameBasedMBeanInfoAssembler">
            <property name="managedMethods">
                <value>add,myOperation,getName,setName,getAge</value>
            </property>
        </bean>
    </property>
</bean>

在前面的示例中,您可以看到addmyOperation方法公开为 JMX 操作,而getName()setName(String)getAge()公开为 JMX 属性的适当一半。在前面的代码中,方法 Map 适用于 JMX 公开的 bean。要控制每个 bean 的方法公开,可以使用MethodNameMBeanInfoAssemblermethodMappings属性将 bean 名称 Map 到方法名称列表。

4.3. 控制您的 Bean 的 ObjectName 实例

在幕后,MBeanExporter委派ObjectNamingStrategy的实现,以为其注册的每个 bean 获得ObjectName实例。默认情况下,默认实现KeyNamingStrategy使用beans Map的键作为ObjectName。此外,KeyNamingStrategy可以将beans Map的键 Map 到Properties文件(或多个文件)中的条目以解析ObjectName。除了KeyNamingStrategy之外,Spring 还提供了两个附加的ObjectNamingStrategy实现:IdentityNamingStrategy(基于 Bean 的 JVM 身份构建ObjectName)和MetadataNamingStrategy(使用源级元数据获取ObjectName)。

4.3.1. 从属性读取 ObjectName 实例

您可以配置自己的KeyNamingStrategy实例,并将其配置为从Properties实例读取ObjectName实例,而不必使用 Bean 键。 KeyNamingStrategy尝试使用与 bean 密钥相对应的密钥在Properties中定位条目。如果未找到任何条目,或者Properties实例是null,则使用 bean 密钥本身。

以下代码显示了KeyNamingStrategy的示例配置:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="testBean" value-ref="testBean"/>
            </map>
        </property>
        <property name="namingStrategy" ref="namingStrategy"/>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

    <bean id="namingStrategy" class="org.springframework.jmx.export.naming.KeyNamingStrategy">
        <property name="mappings">
            <props>
                <prop key="testBean">bean:name=testBean1</prop>
            </props>
        </property>
        <property name="mappingLocations">
            <value>names1.properties,names2.properties</value>
        </property>
    </bean>

</beans>

前面的示例将KeyNamingStrategy实例与Properties实例配置在一起,该实例是从 mapping 属性定义的Properties实例和位于 mappings 属性定义的路径中的属性文件合并而成的。在此配置中,testBean bean 被赋予bean:name=testBean1ObjectName,因为这是Properties实例中的条目,该条目具有与 bean 密钥相对应的密钥。

如果在Properties实例中找不到任何条目,则将 bean 键名用作ObjectName

4.3.2. 使用元数据命名策略

MetadataNamingStrategy使用每个 bean 上ManagedResource属性的objectName属性来创建ObjectName。以下代码显示了MetadataNamingStrategy的配置:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="testBean" value-ref="testBean"/>
            </map>
        </property>
        <property name="namingStrategy" ref="namingStrategy"/>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

    <bean id="namingStrategy" class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
        <property name="attributeSource" ref="attributeSource"/>
    </bean>

    <bean id="attributeSource"
            class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>

</beans>

如果没有为ManagedResource属性提供objectName,那么将使用以下格式创建ObjectName:* [完全合格的软件包名称]:type = [short-classname],name = [bean-name] *。例如,为以下 bean 生成的ObjectName将是com.example:type=MyClass,name=myBean

<bean id="myBean" class="com.example.MyClass"/>

4.3.3. 配置基于 Comments 的 MBean 导出

如果您更喜欢使用基于 Comments 的方法定义 Management 界面,则可以使用MBeanExporter的便利子类:AnnotationMBeanExporter。在定义该子类的实例时,您不再需要namingStrategyassemblerattributeSource配置,因为它始终使用基于 JavaComments 的标准元数据(也始终启用自动检测)。实际上,@EnableMBeanExport @ConfigurationComments 支持更简单的语法,而不是定义MBeanExporter bean,如以下示例所示:

@Configuration
@EnableMBeanExport
public class AppConfig {

}

如果您更喜欢基于 XML 的配置,则<context:mbean-export/>元素具有相同的目的,并在以下列表中显示:

<context:mbean-export/>

如有必要,可以提供对特定 MBean server的引用,并且defaultDomain属性(AnnotationMBeanExporter的属性)接受生成的 MBean ObjectName域的备用值。如上一示例所示,它用于代替上一章节MetadataNamingStrategy中描述的标准软件包名称:

@EnableMBeanExport(server="myMBeanServer", defaultDomain="myDomain")
@Configuration
ContextConfiguration {

}

以下示例显示了与前面的基于 Comments 的示例等效的 XML:

<context:mbean-export server="myMBeanServer" default-domain="myDomain"/>

Warning

请勿将基于接口的 AOP 代理与 bean 类中的 JMXComments 的自动检测结合使用。基于接口的代理“隐藏”目标类,它也隐藏了 JMXManagement 的资源 Comments。因此,在这种情况下,您应该使用目标类代理(通过在<aop:config/><tx:annotation-driven/>等上设置'proxy-target-class'标志)。否则,启动时可能会静默忽略您的 JMX bean。

4.4. 使用 JSR-160 连接器

对于远程访问,Spring JMX 模块在org.springframework.jmx.support包中提供了两个FactoryBean实现,用于创建服务器端和 Client 端连接器。

4.4.1. 服务器端连接器

要使 Spring JMX 创建,启动和公开 JSR-160 JMXConnectorServer,可以使用以下配置:

<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean"/>

默认情况下,ConnectorServerFactoryBean创建绑定到service:jmx:jmxmp://localhost:9875JMXConnectorServer。因此,serverConnector bean 通过 localhost 上的 JMXMP 协议(端口 9875)向 Client 端公开本地MBeanServer。请注意,JSR 160 规范将 JMXMP 协议标记为可选。当前,主要的开源 JMX 实现 MX4J 和 JDK 随附的实现不支持 JMXMP。

要指定另一个 URL 并向MBeanServer注册JMXConnectorServer本身,可以分别使用serviceUrlObjectName属性,如以下示例所示:

<bean id="serverConnector"
        class="org.springframework.jmx.support.ConnectorServerFactoryBean">
    <property name="objectName" value="connector:name=rmi"/>
    <property name="serviceUrl"
            value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/myconnector"/>
</bean>

如果设置了ObjectName属性,Spring 会在ObjectName下自动将连接器注册到MBeanServer。以下示例显示了创建JMXConnector时可以传递给ConnectorServerFactoryBean的完整参数集:

<bean id="serverConnector"
        class="org.springframework.jmx.support.ConnectorServerFactoryBean">
    <property name="objectName" value="connector:name=iiop"/>
    <property name="serviceUrl"
        value="service:jmx:iiop://localhost/jndi/iiop://localhost:900/myconnector"/>
    <property name="threaded" value="true"/>
    <property name="daemon" value="true"/>
    <property name="environment">
        <map>
            <entry key="someKey" value="someValue"/>
        </map>
    </property>
</bean>

请注意,在使用基于 RMI 的连接器时,需要启动查找服务(tnameservrmiregistry)才能完成名称注册。如果您使用 Spring 通过 RMI 为您导出远程服务,则 Spring 已经构造了一个 RMI 注册表。如果没有,您可以使用以下配置片段轻松启动注册表:

<bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
    <property name="port" value="1099"/>
</bean>

4.4.2. Client 端连接器

要为启用了远程 JSR-160 的MBeanServer创建MBeanServerConnection,可以使用MBeanServerConnectionFactoryBean,如以下示例所示:

<bean id="clientConnector" class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
    <property name="serviceUrl" value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"/>
</bean>

4.4.3. 通过 Hessian 或 SOAP 的 JMX

JSR-160 允许扩展 Client 端与服务器之间进行通信的方式。前面各节中显示的示例使用 JSR-160 规范(IIOP 和 JRMP)和(可选)JMXMP 所需的基于 RMI 的强制实现。通过使用其他提供程序或 JMX 实现(例如MX4J),可以通过简单的 HTTP 或 SSL 以及其他协议利用 SOAP 或 Hessian 等协议,如以下示例所示:

<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean">
    <property name="objectName" value="connector:name=burlap"/>
    <property name="serviceUrl" value="service:jmx:burlap://localhost:9874"/>
</bean>

在前面的示例中,我们使用了 MX4J 3.0.0. 有关更多信息,请参见官方 MX4J 文档。

4.5. 通过代理访问 MBean

Spring JMX 使您可以创建代理,以将调用重新路由到在本地或远程MBeanServer中注册的 MBean。这些代理为您提供了一个标准的 Java 接口,您可以通过它与 MBean 进行交互。以下代码显示了如何为在本地MBeanServer中运行的 MBean 配置代理:

<bean id="proxy" class="org.springframework.jmx.access.MBeanProxyFactoryBean">
    <property name="objectName" value="bean:name=testBean"/>
    <property name="proxyInterface" value="org.springframework.jmx.IJmxTestBean"/>
</bean>

在前面的示例中,您可以看到为在bean:name=testBeanObjectName下注册的 MBean 创建了代理。代理实现的接口集由proxyInterfaces属性控制,将这些接口上的方法和属性 Map 到 MBean 上的操作和属性的规则与InterfaceBasedMBeanInfoAssembler使用的规则相同。

MBeanProxyFactoryBean可以创建可通过MBeanServerConnection访问的任何 MBean 的代理。默认情况下,已找到并使用了本地MBeanServer,但是您可以覆盖它并提供一个MBeanServerConnection指向远程MBeanServer,以适应指向远程 MBean 的代理:

<bean id="clientConnector"
        class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
    <property name="serviceUrl" value="service:jmx:rmi://remotehost:9875"/>
</bean>

<bean id="proxy" class="org.springframework.jmx.access.MBeanProxyFactoryBean">
    <property name="objectName" value="bean:name=testBean"/>
    <property name="proxyInterface" value="org.springframework.jmx.IJmxTestBean"/>
    <property name="server" ref="clientConnector"/>
</bean>

在前面的示例中,我们创建一个MBeanServerConnection,该MBeanServerConnection指向使用MBeanServerConnectionFactoryBean的远程计算机。然后,此MBeanServerConnection通过server属性传递到MBeanProxyFactoryBean。创建的代理通过此MBeanServerConnection将所有调用转发到MBeanServer

4.6. Notifications

Spring 的 JMX 产品包括对 JMX 通知的全面支持。

4.6.1. 注册侦听器以接收通知

Spring 的 JMX 支持使您可以轻松地将任意数量的NotificationListeners注册到任意数量的 MBean(这包括 Spring 的MBeanExporter导出的 MBean 和通过其他机制注册的 MBean)。例如,考虑一种情况,即每次目标 MBean 的属性发生更改时,都希望(通过Notification)被告知。以下示例将通知写入控制台:

package com.example;

import javax.management.AttributeChangeNotification;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;

public class ConsoleLoggingNotificationListener
        implements NotificationListener, NotificationFilter {

    public void handleNotification(Notification notification, Object handback) {
        System.out.println(notification);
        System.out.println(handback);
    }

    public boolean isNotificationEnabled(Notification notification) {
        return AttributeChangeNotification.class.isAssignableFrom(notification.getClass());
    }

}

以下示例将ConsoleLoggingNotificationListener(在前面的示例中定义)添加到notificationListenerMappings

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="bean:name=testBean1" value-ref="testBean"/>
            </map>
        </property>
        <property name="notificationListenerMappings">
            <map>
                <entry key="bean:name=testBean1">
                    <bean class="com.example.ConsoleLoggingNotificationListener"/>
                </entry>
            </map>
        </property>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

</beans>

使用先前的配置,每次从目标 MBean(bean:name=testBean1)BroadcastJMX Notification时,都会通知通过notificationListenerMappings属性注册为侦听器的ConsoleLoggingNotificationListener bean。然后ConsoleLoggingNotificationListener bean 可以响应Notification采取其认为适当的任何操作。

您还可以使用纯 bean 名称作为导出的 bean 与侦听器之间的链接,如以下示例所示:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="bean:name=testBean1" value-ref="testBean"/>
            </map>
        </property>
        <property name="notificationListenerMappings">
            <map>
                <entry key="testBean">
                    <bean class="com.example.ConsoleLoggingNotificationListener"/>
                </entry>
            </map>
        </property>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

</beans>

如果要为封闭的MBeanExporter导出的所有 bean 注册一个NotificationListener实例,则可以使用特殊的通配符(*)作为notificationListenerMappings属性 Map 中条目的键,如以下示例所示:

<property name="notificationListenerMappings">
    <map>
        <entry key="*">
            <bean class="com.example.ConsoleLoggingNotificationListener"/>
        </entry>
    </map>
</property>

如果需要进行相反操作(即,针对 MBean 注册多个不同的侦听器),则必须使用notificationListeners list 属性(而不是notificationListenerMappings属性)。这次,我们配置NotificationListenerBean实例,而不是为单个 MBean 配置NotificationListenerNotificationListenerBeanNotificationListener和要注册的ObjectName(或ObjectNames)封装在MBeanServer中。 NotificationListenerBean还封装了许多其他属性,例如NotificationFilter和可以在高级 JMX 通知场景中使用的任意 handback 对象。

使用NotificationListenerBean实例时的配置与先前介绍的配置没有很大不同,如以下示例所示:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="bean:name=testBean1" value-ref="testBean"/>
            </map>
        </property>
        <property name="notificationListeners">
            <list>
                <bean class="org.springframework.jmx.export.NotificationListenerBean">
                    <constructor-arg>
                        <bean class="com.example.ConsoleLoggingNotificationListener"/>
                    </constructor-arg>
                    <property name="mappedObjectNames">
                        <list>
                            <value>bean:name=testBean1</value>
                        </list>
                    </property>
                </bean>
            </list>
        </property>
    </bean>

    <bean id="testBean" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

</beans>

前面的示例等效于第一个通知示例。那么,假设我们想在每次引发Notification时得到一个递归对象,并且还希望通过提供NotificationFilter来过滤掉无关的Notifications。以下示例实现了这些目标:

<beans>

    <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="bean:name=testBean1" value-ref="testBean1"/>
                <entry key="bean:name=testBean2" value-ref="testBean2"/>
            </map>
        </property>
        <property name="notificationListeners">
            <list>
                <bean class="org.springframework.jmx.export.NotificationListenerBean">
                    <constructor-arg ref="customerNotificationListener"/>
                    <property name="mappedObjectNames">
                        <list>
                            <!-- handles notifications from two distinct MBeans -->
                            <value>bean:name=testBean1</value>
                            <value>bean:name=testBean2</value>
                        </list>
                    </property>
                    <property name="handback">
                        <bean class="java.lang.String">
                            <constructor-arg value="This could be anything..."/>
                        </bean>
                    </property>
                    <property name="notificationFilter" ref="customerNotificationListener"/>
                </bean>
            </list>
        </property>
    </bean>

    <!-- implements both the NotificationListener and NotificationFilter interfaces -->
    <bean id="customerNotificationListener" class="com.example.ConsoleLoggingNotificationListener"/>

    <bean id="testBean1" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="TEST"/>
        <property name="age" value="100"/>
    </bean>

    <bean id="testBean2" class="org.springframework.jmx.JmxTestBean">
        <property name="name" value="ANOTHER TEST"/>
        <property name="age" value="200"/>
    </bean>

</beans>

(有关移交对象是什么,实际上NotificationFilter是什么的完整讨论,请参阅 JMX 规范(1.2)中名为“ JMX 通知模型”的部分。)

4.6.2. 发布通知

Spring 不仅提供注册支持以接收Notifications,而且还提供发布Notifications的支持。

Note

这部分实际上仅与已通过MBeanExporter公开为 MBean 的 Spring 托管 Bean 有关。任何现有的用户定义的 MBean 都应使用标准的 JMX API 进行通知发布。

Spring 的 JMX 通知发布支持中的关键接口是NotificationPublisher接口(在org.springframework.jmx.export.notification包中定义)。任何要通过MBeanExporter实例作为 MBean 导出的 bean 都可以实现相关的NotificationPublisherAware接口来访问NotificationPublisher实例。 NotificationPublisherAware接口通过一个简单的 setter 方法将NotificationPublisher的实例提供给实现 Bean,然后该 bean 可以使用它来发布Notifications

NotificationPublisher接口的 javadoc 中所述,通过NotificationPublisher机制发布事件的托管 bean 不负责通知侦听器的状态 Management。 Spring 的 JMX 支持负责处理所有 JMX 基础结构问题。作为应用程序开发人员,您需要做的就是实现NotificationPublisherAware接口并使用提供的NotificationPublisher实例开始发布事件。注意,NotificationPublisher是在已将托管 bean 注册到MBeanServer之后设置的。

使用NotificationPublisher实例非常简单。您创建一个 JMX Notification实例(或适当的Notification子类的实例),使用与要发布的事件相关的数据填充通知,并在NotificationPublisher实例上调用sendNotification(Notification)并传入Notification

在以下示例中,每次调用add(int, int)操作时,JmxTestBean的导出实例都会发布NotificationEvent

package org.springframework.jmx;

import org.springframework.jmx.export.notification.NotificationPublisherAware;
import org.springframework.jmx.export.notification.NotificationPublisher;
import javax.management.Notification;

public class JmxTestBean implements IJmxTestBean, NotificationPublisherAware {

    private String name;
    private int age;
    private boolean isSuperman;
    private NotificationPublisher publisher;

    // other getters and setters omitted for clarity

    public int add(int x, int y) {
        int answer = x + y;
        this.publisher.sendNotification(new Notification("add", this, 0));
        return answer;
    }

    public void dontExposeMe() {
        throw new RuntimeException();
    }

    public void setNotificationPublisher(NotificationPublisher notificationPublisher) {
        this.publisher = notificationPublisher;
    }

}

NotificationPublisher界面和使一切正常运行的机制是 Spring 的 JMX 支持的更好的功能之一。但是,它确实带有将类耦合到 Spring 和 JMX 的代价。和往常一样,这里的建议要务实。如果您需要NotificationPublisher提供的功能,并且可以接受到 Spring 和 JMX 的耦合,则可以这样做。

4.7. 更多资源

本节包含有关 JMX 的更多资源的链接:

5. JCA CCI

Java EE 提供了一个规范来标准化对企业信息系统(EIS)的访问:JCA(Java EE 连接器体系结构)。该规范分为两个不同的部分:

  • 连接器提供程序必须实现的 SPI(服务提供程序接口)。这些接口构成了可以部署在 Java EE 应用程序服务器上的资源适配器。在这种情况下,服务器将 Management 连接池,事务和安全性(托管模式)。应用服务器还负责 Management 配置,该配置保存在 Client 端应用程序外部。连接器也可以在没有应用程序服务器的情况下使用。在这种情况下,应用程序必须直接对其进行配置(非托管模式)。

  • 应用程序可用于与连接器进行交互并由此与 EIS 通信的 CCI(通用 Client 端接口)。还提供了用于本地事务划分的 API。

Spring CCI 支持的目的是使用 Spring Framework 的常规资源和事务 Management 工具,提供类以典型的 Spring 样式访问 CCI 连接器。

Note

连接器的 Client 端始终不使用 CCI。某些连接器公开了自己的 API,提供了 JCA 资源适配器以使用 Java EE 容器的系统协定(连接池,全局事务和安全性)。 Spring 没有为此类特定于连接器的 API 提供特殊支持。

5.1. 配置 CCI

本节介绍如何配置公共 Client 端接口(CCI)。它包括以下主题:

5.1.1. 连接器配置

使用 JCA CCI 的基本资源是ConnectionFactory接口。您使用的连接器必须提供此接口的实现。

要使用连接器,可以将其部署在应用程序服务器上,并从服务器的 JNDI 环境(托管模式)中获取ConnectionFactory。连接器必须打包为 RAR 文件(资源适配器 Files),并包含ra.xml文件来描述其部署特性。资源的实际名称是在部署时指定的。要在 Spring 中访问它,可以使用 Spring 的JndiObjectFactoryBean<jee:jndi-lookup>通过其 JNDI 名称获取工厂。

使用连接器的另一种方法是将其嵌入到您的应用程序中(非托管模式),而不使用应用程序服务器来部署和配置它。 Spring 提供了通过称为(LocalConnectionFactoryBean)的FactoryBean实现将连接器配置为 Bean 的可能性。通过这种方式,您只需要在 Classpath 中使用连接器库(不需要 RAR 文件和ra.xmlDescriptors)。如有必要,必须从连接器的 RAR 文件中提取该库。

一旦可以访问ConnectionFactory实例,就可以将其注入到组件中。这些组件可以根据普通的 CCI API 进行编码,也可以使用 Spring 的支持类进行 CCI 访问(例如CciTemplate)。

Note

在非托管模式下使用连接器时,您将无法使用全局事务,因为资源永远不会在当前线程的当前全局事务中被征用或除名。该资源不知道任何可能正在运行的全局 Java EE 事务。

5.1.2. Spring 中的 ConnectionFactory 配置

要连接到 EIS,需要从应用程序服务器(如果处于托管模式)或直接从 Spring(如果处于非托管模式)获取ConnectionFactory

在托管模式下,您可以从 JNDI 访问ConnectionFactory。其属性在应用程序服务器中配置。以下示例显示了如何执行此操作:

<jee:jndi-lookup id="eciConnectionFactory" jndi-name="eis/cicseci"/>

在非托管模式下,必须将要在 Spring 的配置中使用的ConnectionFactory配置为 JavaBean。 LocalConnectionFactoryBean类提供了这种设置样式,传入了连接器的ManagedConnectionFactory实现,公开了应用程序级 CCI ConnectionFactory。以下示例显示了如何执行此操作:

<bean id="eciManagedConnectionFactory" class="com.ibm.connector2.cics.ECIManagedConnectionFactory">
    <property name="serverName" value="TXSERIES"/>
    <property name="connectionURL" value="tcp://localhost/"/>
    <property name="portNumber" value="2006"/>
</bean>

<bean id="eciConnectionFactory" class="org.springframework.jca.support.LocalConnectionFactoryBean">
    <property name="managedConnectionFactory" ref="eciManagedConnectionFactory"/>
</bean>

Note

您不能直接实例化特定的ConnectionFactory。您需要为连接器完成ManagedConnectionFactory接口的相应实现。该接口是 JCA SPI 规范的一部分。

5.1.3. 配置 CCI 连接

通过 JCA CCI,您可以使用连接器的ConnectionSpec实现来配置到 EIS 的连接。要配置其属性,您需要使用专用适配器ConnectionSpecConnectionFactoryAdapter包装目标连接工厂。您可以使用connectionSpec属性(作为内部 bean)配置专用的ConnectionSpec

此属性不是必需的,因为 CCI ConnectionFactory接口定义了两种不同的方法来获取 CCI 连接。您通常可以在应用程序服务器(处于托管模式下)或相应的本地ManagedConnectionFactory实现上配置某些ConnectionSpec属性。以下 Lists 显示了ConnectionFactory接口定义的相关部分:

public interface ConnectionFactory implements Serializable, Referenceable {
    ...
    Connection getConnection() throws ResourceException;
    Connection getConnection(ConnectionSpec connectionSpec) throws ResourceException;
    ...
}

Spring 提供了一个ConnectionSpecConnectionFactoryAdapter,您可以指定一个ConnectionSpec实例用于给定工厂上的所有操作。如果指定了适配器的connectionSpec属性,则适配器将getConnection变量与ConnectionSpec参数一起使用。否则,适配器将使用不带该参数的变量。以下示例显示了如何配置ConnectionSpecConnectionFactoryAdapter

<bean id="managedConnectionFactory"
        class="com.sun.connector.cciblackbox.CciLocalTxManagedConnectionFactory">
    <property name="connectionURL" value="jdbc:hsqldb:hsql://localhost:9001"/>
    <property name="driverName" value="org.hsqldb.jdbcDriver"/>
</bean>

<bean id="targetConnectionFactory"
        class="org.springframework.jca.support.LocalConnectionFactoryBean">
    <property name="managedConnectionFactory" ref="managedConnectionFactory"/>
</bean>

<bean id="connectionFactory"
        class="org.springframework.jca.cci.connection.ConnectionSpecConnectionFactoryAdapter">
    <property name="targetConnectionFactory" ref="targetConnectionFactory"/>
    <property name="connectionSpec">
        <bean class="com.sun.connector.cciblackbox.CciConnectionSpec">
            <property name="user" value="sa"/>
            <property name="password" value=""/>
        </bean>
    </property>
</bean>

5.1.4. 使用单个 CCI 连接

如果要使用单个 CCI 连接,Spring 会提供另一个ConnectionFactory适配器来 Management 此连接。 SingleConnectionFactory适配器类延迟打开单个连接,并在应用程序关闭时销毁该 bean 时将其关闭。此类公开了具有相应行为的特殊Connection代理,它们均共享相同的基础物理连接。以下示例显示如何使用SingleConnectionFactory适配器类:

<bean id="eciManagedConnectionFactory"
        class="com.ibm.connector2.cics.ECIManagedConnectionFactory">
    <property name="serverName" value="TEST"/>
    <property name="connectionURL" value="tcp://localhost/"/>
    <property name="portNumber" value="2006"/>
</bean>

<bean id="targetEciConnectionFactory"
        class="org.springframework.jca.support.LocalConnectionFactoryBean">
    <property name="managedConnectionFactory" ref="eciManagedConnectionFactory"/>
</bean>

<bean id="eciConnectionFactory"
        class="org.springframework.jca.cci.connection.SingleConnectionFactory">
    <property name="targetConnectionFactory" ref="targetEciConnectionFactory"/>
</bean>

Note

无法使用ConnectionSpec直接配置此ConnectionFactory适配器。如果您需要特定ConnectionSpec的单个连接,则可以使用SingleConnectionFactory与之联系的中介ConnectionSpecConnectionFactoryAdapter

5.2. 使用 Spring 的 CCI 访问支持

本节描述如何使用 Spring 对 CCI 的支持来实现各种目的。它包括以下主题:

5.2.1. 记录转换

Spring 的 JCA CCI 支持的目的之一是为处理 CCI 记录提供便利的设施。您可以指定策略来创建记录并从 Logging 提取数据,以用于 Spring 的CciTemplate。如果您不想直接在应用程序中使用记录,本节中描述的接口将策略配置为使用 Importing 和输出记录。

要创建 ImportingRecord,可以使用RecordCreator接口的专用实现。以下 Lists 显示了RecordCreator接口定义:

public interface RecordCreator {

    Record createRecord(RecordFactory recordFactory) throws ResourceException, DataAccessException;

}

createRecord(..)方法接收RecordFactory实例作为参数,该实例与所使用的ConnectionFactoryRecordFactory相对应。您可以使用此引用来创建IndexedRecordMappedRecord实例。下面的示例演示如何使用RecordCreator接口以及索引或 Map 记录:

public class MyRecordCreator implements RecordCreator {

    public Record createRecord(RecordFactory recordFactory) throws ResourceException {
        IndexedRecord input = recordFactory.createIndexedRecord("input");
        input.add(new Integer(id));
        return input;
    }

}

您可以使用输出Record从 EIS 接收数据。因此,您可以将RecordExtractor接口的特定实现传递给 Spring 的CciTemplate,以从输出Record提取数据。以下 Lists 显示了RecordExtractor接口定义:

public interface RecordExtractor {

    Object extractData(Record record) throws ResourceException, SQLException, DataAccessException;

}

以下示例显示了如何使用RecordExtractor界面:

public class MyRecordExtractor implements RecordExtractor {

    public Object extractData(Record record) throws ResourceException {
        CommAreaRecord commAreaRecord = (CommAreaRecord) record;
        String str = new String(commAreaRecord.toByteArray());
        String field1 = string.substring(0,6);
        String field2 = string.substring(6,1);
        return new OutputObject(Long.parseLong(field1), field2);
    }

}

5.2.2. 使用 CciTemplate

CciTemplate是核心 CCI 支持包(org.springframework.jca.cci.core)的中心类。由于它处理资源的创建和释放,因此它简化了 CCI 的使用。这有助于避免常见错误,例如忘记始终关闭连接。它关心连接和交互对象的生命周期,让应用程序代码专注于从应用程序数据生成 Importing 记录并从输出 Logging 提取应用程序数据。

JCA CCI 规范定义了两种不同的方法来调用 EIS 上的操作。 CCI Interaction接口提供了两个 execute 方法签名,如以下 Lists 所示:

public interface javax.resource.cci.Interaction {

    ...

    boolean execute(InteractionSpec spec, Record input, Record output) throws ResourceException;

    Record execute(InteractionSpec spec, Record input) throws ResourceException;

    ...

}

根据所调用的模板方法,CciTemplate知道在交互中要调用哪个execute方法。无论如何,必须正确初始化InteractionSpec实例。

您可以通过两种方式使用CciTemplate.execute(..)

  • 具有直接的Record参数。在这种情况下,您需要传递 CCIImporting 记录,并且返回的对象是相应的 CCI 输出记录。

  • 对于应用程序对象,通过使用记录 Map。在这种情况下,您需要提供相应的RecordCreatorRecordExtractor实例。

对于第一种方法,使用模板的以下方法(直接与Interaction接口上的方法相对应):

public class CciTemplate implements CciOperations {

    public Record execute(InteractionSpec spec, Record inputRecord)
            throws DataAccessException { ... }

    public void execute(InteractionSpec spec, Record inputRecord, Record outputRecord)
            throws DataAccessException { ... }

}

使用第二种方法,我们需要指定记录创建和记录提取策略作为参数。使用的接口是上一节有关记录转换中描述的接口。下面的 Lists 显示了相应的CciTemplate方法:

public class CciTemplate implements CciOperations {

    public Record execute(InteractionSpec spec,
            RecordCreator inputCreator) throws DataAccessException {
        // ...
    }

    public Object execute(InteractionSpec spec, Record inputRecord,
            RecordExtractor outputExtractor) throws DataAccessException {
        // ...
    }

    public Object execute(InteractionSpec spec, RecordCreator creator,
            RecordExtractor extractor) throws DataAccessException {
        // ...
    }

}

除非在模板上设置了outputRecordCreator属性(请参见下一节),否则每个方法都将使用两个参数调用InteractionSpec和 ImportingRecord来调用 CCI Interaction的相应execute方法。它接收输出Record作为其返回值。

CciTemplate还提供了通过createIndexRecord(..)createMappedRecord(..)方法在RecordCreator实现之外创建IndexRecordMappedRecord的方法。您可以在 DAO 实现中使用它来创建Record实例以传递到相应的CciTemplate.execute(..)方法中。以下 Lists 显示了CciTemplate接口定义:

public class CciTemplate implements CciOperations {

    public IndexedRecord createIndexedRecord(String name) throws DataAccessException { ... }

    public MappedRecord createMappedRecord(String name) throws DataAccessException { ... }

}

5.2.3. 使用 DAO 支持

Spring 的 CCI 支持为 DAO 提供了一个抽象类,支持注入ConnectionFactoryCciTemplate实例。该类的名称是CciDaoSupport。它提供了简单的setConnectionFactorysetCciTemplate方法。在内部,此类为传入的ConnectionFactory创建CciTemplate实例,并将其暴露给子类中的具体数据访问实现。以下示例显示了如何使用CciDaoSupport

public abstract class CciDaoSupport {

    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        // ...
    }

    public ConnectionFactory getConnectionFactory() {
        // ...
    }

    public void setCciTemplate(CciTemplate cciTemplate) {
        // ...
    }

    public CciTemplate getCciTemplate() {
        // ...
    }

}

5.2.4. 自动输出记录生成

如果您使用的连接器仅支持将 Importing 和输出记录作为参数的Interaction.execute(..)方法(也就是说,它需要传递所需的输出记录而不是返回适当的输出记录),则可以将CciTemplateoutputRecordCreator属性设置为收到响应后,将自动生成输出记录,以供 JCA 连接器填充。然后将该记录返回给模板的调用者。

此属性保存用于该目的的RecordCreator interface的实现。您必须直接在CciTemplate上指定outputRecordCreator属性。以下示例显示了如何执行此操作:

cciTemplate.setOutputRecordCreator(new EciOutputRecordCreator());

或者(建议使用这种方法),在 Spring 配置中,如果将CciTemplate配置为专用 bean 实例,则可以按以下方式定义 bean:

<bean id="eciOutputRecordCreator" class="eci.EciOutputRecordCreator"/>

<bean id="cciTemplate" class="org.springframework.jca.cci.core.CciTemplate">
    <property name="connectionFactory" ref="eciConnectionFactory"/>
    <property name="outputRecordCreator" ref="eciOutputRecordCreator"/>
</bean>

Note

由于CciTemplate类是线程安全的,因此通常将其配置为共享实例。

5.2.5. CciTemplate 交互摘要

下表总结了CciTemplate类的机制以及在 CCI Interaction接口上调用的相应方法:

表 9.交互执行方法的用法

CciTemplate方法签名CciTemplate outputRecordCreator属性execute在 CCI 交互上调用的方法
Record execute(InteractionSpec, Record)Not setRecord execute(InteractionSpec, Record)
Record execute(InteractionSpec, Record)Setboolean execute(InteractionSpec, Record, Record)
无效执行(InteractionSpec,记录,记录)Not set无效执行(InteractionSpec,记录,记录)
void execute(InteractionSpec, Record, Record)Setvoid execute(InteractionSpec, Record, Record)
Record execute(InteractionSpec, RecordCreator)Not setRecord execute(InteractionSpec, Record)
Record execute(InteractionSpec, RecordCreator)Setvoid execute(InteractionSpec, Record, Record)
Record execute(InteractionSpec, Record, RecordExtractor)Not setRecord execute(InteractionSpec, Record)
Record execute(InteractionSpec, Record, RecordExtractor)Setvoid execute(InteractionSpec, Record, Record)
Record execute(InteractionSpec, RecordCreator, RecordExtractor)Not setRecord execute(InteractionSpec, Record)
Record execute(InteractionSpec, RecordCreator, RecordExtractor)Setvoid execute(InteractionSpec, Record, Record)

5.2.6. 直接使用 CCI 连接和交互

CciTemplate还允许您以与JdbcTemplateJmsTemplate相同的方式直接处理 CCI 连接和交互。例如,当您要对 CCI 连接或交互执行多个操作时,此功能很有用。

ConnectionCallback接口提供 CCI Connection作为自变量(以对其执行自定义操作)以及创建Connection的 CCI ConnectionFactory。后者可能很有用(例如,获取关联的RecordFactory实例并创建索引/Map 记录)。以下 Lists 显示了ConnectionCallback接口定义:

public interface ConnectionCallback {

    Object doInConnection(Connection connection, ConnectionFactory connectionFactory)
            throws ResourceException, SQLException, DataAccessException;

}

InteractionCallback接口提供 CCI Interaction(以对其执行自定义操作)以及相应的 CCI ConnectionFactory。以下 Lists 显示了InteractionCallback接口定义:

public interface InteractionCallback {

    Object doInInteraction(Interaction interaction, ConnectionFactory connectionFactory)
        throws ResourceException, SQLException, DataAccessException;

}

Note

InteractionSpec对象可以在多个模板调用之间共享,也可以在每个回调方法中重新创建。这完全取决于 DAO 的实现。

5.2.7. CciTemplate 用法示例

在本部分中,我们将说明CciTemplate通过 IBM CICS ECI 连接器以 ECI 模式访问 CICS 的用法。

首先,我们必须对 CCI InteractionSpec进行一些初始化,以指定要访问的 CICS 程序以及如何与之交互,如以下示例所示:

ECIInteractionSpec interactionSpec = new ECIInteractionSpec();
interactionSpec.setFunctionName("MYPROG");
interactionSpec.setInteractionVerb(ECIInteractionSpec.SYNC_SEND_RECEIVE);

然后程序可以通过 Spring 的模板使用 CCI 并指定自定义对象和 CCI Records之间的 Map,如以下示例所示:

public class MyDaoImpl extends CciDaoSupport implements MyDao {

    public OutputObject getData(InputObject input) {
        ECIInteractionSpec interactionSpec = ...;

    OutputObject output = (ObjectOutput) getCciTemplate().execute(interactionSpec,
        new RecordCreator() {
            public Record createRecord(RecordFactory recordFactory) throws ResourceException {
                return new CommAreaRecord(input.toString().getBytes());
            }
        },
        new RecordExtractor() {
            public Object extractData(Record record) throws ResourceException {
                CommAreaRecord commAreaRecord = (CommAreaRecord)record;
                String str = new String(commAreaRecord.toByteArray());
                String field1 = string.substring(0,6);
                String field2 = string.substring(6,1);
                return new OutputObject(Long.parseLong(field1), field2);
            }
        });

        return output;
    }
}

如前所述,您可以使用回调直接在 CCI 连接或交互上工作。以下示例显示了如何执行此操作:

public class MyDaoImpl extends CciDaoSupport implements MyDao {

    public OutputObject getData(InputObject input) {
        ObjectOutput output = (ObjectOutput) getCciTemplate().execute(
            new ConnectionCallback() {
                public Object doInConnection(Connection connection,
                        ConnectionFactory factory) throws ResourceException {

                    // do something...

                }
            });
        }
        return output;
    }

}

Note

对于ConnectionCallback,使用的ConnectionCciTemplateManagement 和关闭,但是回调实现必须 Management 在连接上创建的所有交互。

对于更具体的回调,您可以实现InteractionCallback。如果这样做,传入的Interaction将由CciTemplateManagement 和关闭。以下示例显示了如何执行此操作:

public class MyDaoImpl extends CciDaoSupport implements MyDao {

    public String getData(String input) {
        ECIInteractionSpec interactionSpec = ...;
        String output = (String) getCciTemplate().execute(interactionSpec,
            new InteractionCallback() {
                public Object doInInteraction(Interaction interaction,
                        ConnectionFactory factory) throws ResourceException {
                    Record input = new CommAreaRecord(inputString.getBytes());
                    Record output = new CommAreaRecord();
                    interaction.execute(holder.getInteractionSpec(), input, output);
                    return new String(output.toByteArray());
                }
            });
        return output;
    }

}

对于前面的示例,在非托管模式下,所涉及的 Spring Bean 的相应配置可能类似于以下示例:

<bean id="managedConnectionFactory" class="com.ibm.connector2.cics.ECIManagedConnectionFactory">
    <property name="serverName" value="TXSERIES"/>
    <property name="connectionURL" value="local:"/>
    <property name="userName" value="CICSUSER"/>
    <property name="password" value="CICS"/>
</bean>

<bean id="connectionFactory" class="org.springframework.jca.support.LocalConnectionFactoryBean">
    <property name="managedConnectionFactory" ref="managedConnectionFactory"/>
</bean>

<bean id="component" class="mypackage.MyDaoImpl">
    <property name="connectionFactory" ref="connectionFactory"/>
</bean>

在托管模式下(即,在 Java EE 环境中),配置可能类似于以下示例:

<jee:jndi-lookup id="connectionFactory" jndi-name="eis/cicseci"/>

<bean id="component" class="MyDaoImpl">
    <property name="connectionFactory" ref="connectionFactory"/>
</bean>

5.3. 将 CCI 访问建模为操作对象

org.springframework.jca.cci.object软件包包含支持类,这些支持类使您可以以不同的方式访问 EIS:通过可重用的操作对象,类似于 Spring 的 JDBC 操作对象(请参见数据访问一章的 JDBC 部分)。这通常封装了 CCI API。应用程序级 Importing 对象被传递给操作对象,因此它可以构造 Importing 记录,然后将接收到的记录数据转换为应用程序级输出对象并返回它。

Note

这种方法在内部基于CciTemplate类和RecordCreatorRecordExtractor接口,重用了 Spring 核心 CCI 支持的机制。

5.3.1. 使用 MappingRecordOperation

MappingRecordOperation本质上与CciTemplate执行相同的工作,但代表一个特定的,预先配置的操作作为对象。它提供了两种模板方法来指定如何将 Importing 对象转换为 Importing 记录以及如何将输出记录转换为输出对象(记录 Map):

  • createInputRecord(..):指定如何将 Importing 对象转换为 ImportingRecord

  • extractOutputData(..):指定如何从输出Record中提取输出对象

以下 Lists 显示了这些方法的签名:

public abstract class MappingRecordOperation extends EisOperation {

    ...

    protected abstract Record createInputRecord(RecordFactory recordFactory,
            Object inputObject) throws ResourceException, DataAccessException {
        // ...
    }

    protected abstract Object extractOutputData(Record outputRecord)
            throws ResourceException, SQLException, DataAccessException {
        // ...
    }

    ...

}

之后,要执行 EIS 操作,您需要使用单个execute方法,传入应用程序级 Importing 对象并接收应用程序级输出对象作为结果。以下示例显示了如何执行此操作:

public abstract class MappingRecordOperation extends EisOperation {

    ...

    public Object execute(Object inputObject) throws DataAccessException {
    }

    ...
}

CciTemplate类相反,此execute(..)方法没有InteractionSpec作为参数。相反,InteractionSpec对操作是全局的。您必须使用以下构造函数实例化具有特定InteractionSpec的操作对象。以下示例显示了如何执行此操作:

InteractionSpec spec = ...;
MyMappingRecordOperation eisOperation = new MyMappingRecordOperation(getConnectionFactory(), spec);
...

5.3.2. 使用 MappingCommAreaOperation

一些连接器使用基于 COMMAREA 的记录,该记录表示一个字节数组,其中包含要发送到 EIS 的参数以及它返回的数据。 Spring 提供了一个特殊的操作类,可以直接在 COMMAREA 上工作而不是在记录上工作。 MappingCommAreaOperation类扩展了MappingRecordOperation类以提供此特殊的 COMMAREA 支持。它隐式地使用CommAreaRecord类作为 Importing 和输出记录类型,并提供了两种新方法将 Importing 对象转换为 ImportingCOMMAREA 并将输出 COMMAREA 转换为输出对象。以下 Lists 显示了相关的方法签名:

public abstract class MappingCommAreaOperation extends MappingRecordOperation {

    ...

    protected abstract byte[] objectToBytes(Object inObject)
            throws IOException, DataAccessException;

    protected abstract Object bytesToObject(byte[] bytes)
        throws IOException, DataAccessException;

    ...

}

5.3.3. 自动输出记录生成

由于每个MappingRecordOperation子类内部都基于 CciTemplate,因此可以使用与CciTemplate相同的自动生成输出记录的方式。每个操作对象都提供相应的setOutputRecordCreator(..)方法。有关更多信息,请参见自动输出记录生成

5.3.4. Summary

操作对象方法以与CciTemplate类相同的方式使用记录。

表 10.交互执行方法的用法

MappingRecordOperation方法签名MappingRecordOperation outputRecordCreator属性execute在 CCI 交互上调用的方法
Object execute(Object)Not setRecord execute(InteractionSpec, Record)
Object execute(Object)Setboolean execute(InteractionSpec, Record, Record)

5.3.5. MappingRecordOperation 用法示例

在本节中,我们显示如何使用MappingRecordOperation通过 Blackbox CCI 连接器访问数据库。

Note

该连接器的原始版本由 Java EE SDK(1.3 版)提供,可以从 Oracle 获得。

首先,您必须对 CCI InteractionSpec进行一些初始化以指定要执行的 SQL 请求。在以下示例中,我们直接定义将请求的参数转换为 CCI 记录的方法以及将 CCI 结果记录转换为Person类的实例的方法:

public class PersonMappingOperation extends MappingRecordOperation {

    public PersonMappingOperation(ConnectionFactory connectionFactory) {
        setConnectionFactory(connectionFactory);
        CciInteractionSpec interactionSpec = new CciConnectionSpec();
        interactionSpec.setSql("select * from person where person_id=?");
        setInteractionSpec(interactionSpec);
    }

    protected Record createInputRecord(RecordFactory recordFactory,
            Object inputObject) throws ResourceException {
        Integer id = (Integer) inputObject;
        IndexedRecord input = recordFactory.createIndexedRecord("input");
        input.add(new Integer(id));
        return input;
    }

    protected Object extractOutputData(Record outputRecord)
            throws ResourceException, SQLException {
        ResultSet rs = (ResultSet) outputRecord;
        Person person = null;
        if (rs.next()) {
            Person person = new Person();
            person.setId(rs.getInt("person_id"));
            person.setLastName(rs.getString("person_last_name"));
            person.setFirstName(rs.getString("person_first_name"));
        }
        return person;
    }
}

然后,应用程序可以使用人员标识符作为参数来执行操作对象。请注意,您可以将操作对象设置为共享实例,因为它是线程安全的。下面以人员标识符作为参数执行操作对象:

public class MyDaoImpl extends CciDaoSupport implements MyDao {

    public Person getPerson(int id) {
        PersonMappingOperation query = new PersonMappingOperation(getConnectionFactory());
        Person person = (Person) query.execute(new Integer(id));
        return person;
    }
}

在非托管模式下,Spring Bean 的相应配置如下:

<bean id="managedConnectionFactory"
        class="com.sun.connector.cciblackbox.CciLocalTxManagedConnectionFactory">
    <property name="connectionURL" value="jdbc:hsqldb:hsql://localhost:9001"/>
    <property name="driverName" value="org.hsqldb.jdbcDriver"/>
</bean>

<bean id="targetConnectionFactory"
        class="org.springframework.jca.support.LocalConnectionFactoryBean">
    <property name="managedConnectionFactory" ref="managedConnectionFactory"/>
</bean>

<bean id="connectionFactory"
        class="org.springframework.jca.cci.connection.ConnectionSpecConnectionFactoryAdapter">
    <property name="targetConnectionFactory" ref="targetConnectionFactory"/>
    <property name="connectionSpec">
        <bean class="com.sun.connector.cciblackbox.CciConnectionSpec">
            <property name="user" value="sa"/>
            <property name="password" value=""/>
        </bean>
    </property>
</bean>

<bean id="component" class="MyDaoImpl">
    <property name="connectionFactory" ref="connectionFactory"/>
</bean>

在托管模式下(即,在 Java EE 环境中),配置可以如下:

<jee:jndi-lookup id="targetConnectionFactory" jndi-name="eis/blackbox"/>

<bean id="connectionFactory"
        class="org.springframework.jca.cci.connection.ConnectionSpecConnectionFactoryAdapter">
    <property name="targetConnectionFactory" ref="targetConnectionFactory"/>
    <property name="connectionSpec">
        <bean class="com.sun.connector.cciblackbox.CciConnectionSpec">
            <property name="user" value="sa"/>
            <property name="password" value=""/>
        </bean>
    </property>
</bean>

<bean id="component" class="MyDaoImpl">
    <property name="connectionFactory" ref="connectionFactory"/>
</bean>

5.3.6. MappingCommAreaOperation 用法示例

在本部分中,我们将说明如何使用MappingCommAreaOperation的用法通过 IBM CICS ECI 连接器以 ECI 模式访问 CICS。

首先,我们需要初始化 CCI InteractionSpec以指定要访问哪个 CICS 程序以及如何与之交互,如以下示例所示:

public abstract class EciMappingOperation extends MappingCommAreaOperation {

    public EciMappingOperation(ConnectionFactory connectionFactory, String programName) {
        setConnectionFactory(connectionFactory);
        ECIInteractionSpec interactionSpec = new ECIInteractionSpec(),
        interactionSpec.setFunctionName(programName);
        interactionSpec.setInteractionVerb(ECIInteractionSpec.SYNC_SEND_RECEIVE);
        interactionSpec.setCommareaLength(30);
        setInteractionSpec(interactionSpec);
        setOutputRecordCreator(new EciOutputRecordCreator());
    }

    private static class EciOutputRecordCreator implements RecordCreator {
        public Record createRecord(RecordFactory recordFactory) throws ResourceException {
            return new CommAreaRecord();
        }
    }

}

然后,我们可以继承抽象的EciMappingOperation类,以指定自定义对象和Records之间的 Map,如以下示例所示:

public class MyDaoImpl extends CciDaoSupport implements MyDao {

    public OutputObject getData(Integer id) {
        EciMappingOperation query = new EciMappingOperation(getConnectionFactory(), "MYPROG") {

            protected abstract byte[] objectToBytes(Object inObject) throws IOException {
                Integer id = (Integer) inObject;
                return String.valueOf(id);
            }

            protected abstract Object bytesToObject(byte[] bytes) throws IOException;
                String str = new String(bytes);
                String field1 = str.substring(0,6);
                String field2 = str.substring(6,1);
                String field3 = str.substring(7,1);
                return new OutputObject(field1, field2, field3);
            }
        });

        return (OutputObject) query.execute(new Integer(id));
    }

}

在非托管模式下,Spring Bean 的相应配置如下:

<bean id="managedConnectionFactory" class="com.ibm.connector2.cics.ECIManagedConnectionFactory">
    <property name="serverName" value="TXSERIES"/>
    <property name="connectionURL" value="local:"/>
    <property name="userName" value="CICSUSER"/>
    <property name="password" value="CICS"/>
</bean>

<bean id="connectionFactory" class="org.springframework.jca.support.LocalConnectionFactoryBean">
    <property name="managedConnectionFactory" ref="managedConnectionFactory"/>
</bean>

<bean id="component" class="MyDaoImpl">
    <property name="connectionFactory" ref="connectionFactory"/>
</bean>

在托管模式下(即,在 Java EE 环境中),配置可以如下:

<jee:jndi-lookup id="connectionFactory" jndi-name="eis/cicseci"/>

<bean id="component" class="MyDaoImpl">
    <property name="connectionFactory" ref="connectionFactory"/>
</bean>

5.4. Transactions

JCA 为资源适配器指定了多个级别的事务支持。资源适配器支持的事务类型在其ra.xml文件中指定。本质上有三个选项:无(例如,使用 CICS EPI 连接器),本地事务(例如,使用 CICS ECI 连接器)和全局事务(例如,使用 IMS 连接器)。以下示例配置了全局选项:

<connector>
    <resourceadapter>
        <!-- <transaction-support>NoTransaction</transaction-support> -->
        <!-- <transaction-support>LocalTransaction</transaction-support> -->
        <transaction-support>XATransaction</transaction-support>
    <resourceadapter>
<connector>

对于全局事务,您可以使用 Spring 的通用事务基础结构来划分事务,以JtaTransactionManager作为后端(委托给下面的 Java EE 服务器的分布式事务处理协调器)。

对于单个 CCI ConnectionFactory上的本地事务,Spring 为 CCI 提供了一种特定的事务 Management 策略,类似于 JDBC 的DataSourceTransactionManager。 CCI API 定义了本地事务对象和相应的本地事务划分方法。 Spring 的CciLocalTransactionManager以完全符合 Spring 的通用PlatformTransactionManager抽象的方式执行此类本地 CCI 事务。以下示例配置CciLocalTransactionManager

<jee:jndi-lookup id="eciConnectionFactory" jndi-name="eis/cicseci"/>

<bean id="eciTransactionManager"
        class="org.springframework.jca.cci.connection.CciLocalTransactionManager">
    <property name="connectionFactory" ref="eciConnectionFactory"/>
</bean>

您可以将这两种事务策略与 Spring 的任何事务划分工具一起使用,无论是声明式还是程序式。这是 Spring 通用的PlatformTransactionManager抽象的结果,该抽象将事务划分与实际执行策略分离。您可以根据需要在JtaTransactionManagerCciLocalTransactionManager之间切换,从而保持事务划分不变。

有关 SpringTransaction 功能的更多信息,请参见Transaction Management

6. Email

本节介绍如何使用 Spring Framework 发送电子邮件。

Library dependencies

为了使用 Spring Framework 的电子邮件库,以下 JAR 必须位于应用程序的 Classpath 中:

该库可以在 Web 上免费使用,例如在 Maven Central 中为com.sun.mail:javax.mail

Spring 框架提供了一个有用的 Util 库,用于发送电子邮件,使您不受底层邮件系统的限制,并负责代表 Client 端进行低级资源处理。

org.springframework.mail软件包是 Spring 框架的电子邮件支持的根级软件包。发送电子邮件的中央接口是MailSender接口。封装fromto(以及许多其他邮件)之类的简单邮件的属性的简单值对象是SimpleMailMessage类。该软件包还包含已检查异常的层次结构,该层次结构提供了比较低级别的邮件系统异常更高的抽象级别,根异常为MailException。有关富邮件异常层次结构的更多信息,请参见javadoc

org.springframework.mail.javamail.JavaMailSender接口向MailSender接口(从中继承)提供了特殊的 JavaMail 功能,例如 MIME 消息支持。 JavaMailSender还提供了称为org.springframework.mail.javamail.MimeMessagePreparator的回调接口,用于准备MimeMessage

6.1. Usage

假设我们有一个名为OrderManager的业务接口,如以下示例所示:

public interface OrderManager {

    void placeOrder(Order order);

}

进一步假设我们有一个要求,说明需要生成带有订单号的电子邮件消息并将其发送给下订单的 Client。

6.1.1. MailSender 和 SimpleMailMessage 的基本用法

以下示例显示了有人下订单时如何使用MailSenderSimpleMailMessage发送电子邮件:

import org.springframework.mail.MailException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;

public class SimpleOrderManager implements OrderManager {

    private MailSender mailSender;
    private SimpleMailMessage templateMessage;

    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void setTemplateMessage(SimpleMailMessage templateMessage) {
        this.templateMessage = templateMessage;
    }

    public void placeOrder(Order order) {

        // Do the business calculations...

        // Call the collaborators to persist the order...

        // Create a thread safe "copy" of the template message and customize it
        SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage);
        msg.setTo(order.getCustomer().getEmailAddress());
        msg.setText(
            "Dear " + order.getCustomer().getFirstName()
                + order.getCustomer().getLastName()
                + ", thank you for placing order. Your order number is "
                + order.getOrderNumber());
        try{
            this.mailSender.send(msg);
        }
        catch (MailException ex) {
            // simply log it and go on...
            System.err.println(ex.getMessage());
        }
    }

}

以下示例显示了上述代码的 bean 定义:

<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
    <property name="host" value="mail.mycompany.com"/>
</bean>

<!-- this is a template message that we can pre-load with default state -->
<bean id="templateMessage" class="org.springframework.mail.SimpleMailMessage">
    <property name="from" value="[emailprotected]"/>
    <property name="subject" value="Your order"/>
</bean>

<bean id="orderManager" class="com.mycompany.businessapp.support.SimpleOrderManager">
    <property name="mailSender" ref="mailSender"/>
    <property name="templateMessage" ref="templateMessage"/>
</bean>

6.1.2. 使用 JavaMailSender 和 MimeMessagePreparator

本节描述了使用MimeMessagePreparator回调接口的OrderManager的另一种实现。在下面的示例中,mailSender属性的类型为JavaMailSender,因此我们可以使用 JavaMail MimeMessage类:

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import javax.mail.internet.MimeMessage;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessagePreparator;

public class SimpleOrderManager implements OrderManager {

    private JavaMailSender mailSender;

    public void setMailSender(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void placeOrder(final Order order) {
        // Do the business calculations...
        // Call the collaborators to persist the order...

        MimeMessagePreparator preparator = new MimeMessagePreparator() {
            public void prepare(MimeMessage mimeMessage) throws Exception {
                mimeMessage.setRecipient(Message.RecipientType.TO,
                        new InternetAddress(order.getCustomer().getEmailAddress()));
                mimeMessage.setFrom(new InternetAddress("[emailprotected]"));
                mimeMessage.setText("Dear " + order.getCustomer().getFirstName() + " " +
                        order.getCustomer().getLastName() + ", thanks for your order. " +
                        "Your order number is " + order.getOrderNumber() + ".");
            }
        };

        try {
            this.mailSender.send(preparator);
        }
        catch (MailException ex) {
            // simply log it and go on...
            System.err.println(ex.getMessage());
        }
    }

}

Note

邮件代码是一个横切关注点,很可能是重构为自定义 Spring AOP 方面的候选者,然后可以在OrderManager目标上的适当连接点处执行。

Spring Framework 的邮件支持随附于标准 JavaMail 实现。有关更多信息,请参见相关的 Javadoc。

6.2. 使用 JavaMail MimeMessageHelper

处理 JavaMail 消息时非常方便的类是org.springframework.mail.javamail.MimeMessageHelper,这使您不必使用冗长的 JavaMail API。使用MimeMessageHelper,很容易创建MimeMessage,如以下示例所示:

// of course you would use DI in any real-world cases
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");

MimeMessage message = sender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setTo("[emailprotected]");
helper.setText("Thank you for ordering!");

sender.send(message);

6.2.1. 发送附件和内联资源

Multipart 电子邮件允许同时使用附件和内联资源。内联资源的示例包括您要在邮件中使用但不希望显示为附件的图像或样式表。

Attachments

下面的示例向您展示如何使用MimeMessageHelper发送带有单个 JPEG 图像附件的电子邮件:

JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");

MimeMessage message = sender.createMimeMessage();

// use the true flag to indicate you need a multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("[emailprotected]");

helper.setText("Check out this image!");

// let's attach the infamous windows Sample file (this time copied to c:/)
FileSystemResource file = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addAttachment("CoolImage.jpg", file);

sender.send(message);
Inline Resources

下面的示例向您展示如何使用MimeMessageHelper发送带有嵌入式图像的电子邮件:

JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");

MimeMessage message = sender.createMimeMessage();

// use the true flag to indicate you need a multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("[emailprotected]");

// use the true flag to indicate the text included is HTML
helper.setText("<html><body><img src='cid:identifier1234'></body></html>", true);

// let's include the infamous windows Sample file (this time copied to c:/)
FileSystemResource res = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addInline("identifier1234", res);

sender.send(message);

Warning

内联资源通过使用指定的Content-ID(在上例中为identifier1234)添加到MimeMessage。添加文本和资源的 Sequences 非常重要。确保首先添加文本,然后添加资源。如果您正相反进行操作,则此操作无效。

6.2.2. 使用模板库创建电子邮件内容

上一节中显示的示例中的代码通过使用诸如message.setText(..)之类的方法调用显式创建了电子邮件的内容。这对于简单的情况很好,并且在上述示例的上下文中也可以,其目的是向您展示 API 的基本知识。

但是,在典型的企业应用程序中,由于多种原因,开发人员通常不使用以前显示的方法来创建电子邮件的内容:

  • 用 Java 代码创建基于 HTML 的电子邮件内容很繁琐且容易出错。

  • 显示逻辑和业务逻辑之间没有明确区分。

  • 更改电子邮件内容的显示结构需要编写 Java 代码,重新编译,重新部署等。

通常,解决这些问题的方法是使用模板库(例如 FreeMarker)来定义电子邮件内容的显示结构。这使您的代码只能执行创建要在电子邮件模板中呈现的数据并发送电子邮件的任务。当您的电子邮件的内容变得相当复杂时,这绝对是一种最佳实践,而且,借助 Spring Framework 的 FreeMarker 支持类,它变得非常容易实现。

7.任务执行和计划

Spring 框架分别通过TaskExecutorTaskScheduler接口为任务的异步执行和调度提供了抽象。 Spring 还提供了那些接口的实现,这些接口在应用程序服务器环境中支持线程池或委托给 CommonJ。最终,在公共接口后面使用这些实现可以抽象化 Java SE 5,Java SE 6 和 Java EE 环境之间的差异。

Spring 还具有集成类,以支持Timer(自 1.3 起成为 JDK 的一部分)和 Quartz Scheduler(http://quartz-scheduler.org)进行调度。您可以使用FactoryBean分别设置对TimerTrigger实例的可选引用来设置这两个调度程序。此外,还提供了 Quartz Scheduler 和Timer的便捷类,使您可以调用现有目标对象的方法(类似于正常的MethodInvokingFactoryBean操作)。

7.1. Spring TaskExecutor 抽象

执行程序是线程池概念的 JDK 名称。 “执行程序”的命名是由于不能保证基础实现实际上是一个池。执行程序可能是单线程的,甚至是同步的。 Spring 的抽象隐藏了 Java SE 和 Java EE 环境之间的实现细节。

Spring 的TaskExecutor界面与java.util.concurrent.Executor界面相同。实际上,最初,其存在的主要原因是在使用线程池时抽象出对 Java 5 的需求。该接口具有单个方法(execute(Runnable task)),该方法根据线程池的语义和配置接受要执行的任务。

TaskExecutor最初是为了在需要时为其他 Spring 组件提供线程池抽象而创建的。 ApplicationEventMulticaster,JMS 的AbstractMessageListenerContainer和 Quartz 集成之类的组件都使用TaskExecutor抽象来池化线程。但是,如果您的 bean 需要线程池行为,则也可以根据自己的需要使用此抽象。

7.1.1. TaskExecutor 类型

Spring 包含许多TaskExecutor的预构建实现。您极有可能无需实现自己的方法。 Spring 提供的变体如下:

  • SyncTaskExecutor:此实现不会异步执行调用。而是,每个调用都在调用线程中进行。它主要用于不需要多线程的情况下,例如在简单的测试案例中。

  • SimpleAsyncTaskExecutor:此实现不重用任何线程。而是,它为每次调用启动一个新线程。但是,它确实支持并发限制,该限制会阻止超出限制的所有调用,直到释放插槽为止。如果您正在寻找 true 的池,请参阅ThreadPoolTaskExecutor,在此列表的后面。

  • ConcurrentTaskExecutor:此实现是java.util.concurrent.Executor实例的适配器。还有一个替代方法(ThreadPoolTaskExecutor),它将Executor配置参数公开为 bean 属性。很少需要直接使用ConcurrentTaskExecutor。但是,如果ThreadPoolTaskExecutor不够灵活,无法满足您的需求,则可以选择ConcurrentTaskExecutor

  • ThreadPoolTaskExecutor:最常使用此实现。它公开了用于配置java.util.concurrent.ThreadPoolExecutor的 bean 属性,并将其包装在TaskExecutor中。如果您需要适应另一种java.util.concurrent.Executor,我们建议您使用ConcurrentTaskExecutor代替。

  • WorkManagerTaskExecutor:此实现使用 CommonJ WorkManager作为其支持服务提供者,并且是在 Spring 应用程序上下文中的 WebLogic 或 WebSphere 上设置基于 CommonJ 的线程池集成的中心便利类。

  • DefaultManagedTaskExecutor:此实现在兼容 JSR-236 的运行时环境(例如 Java EE 7 应用程序服务器)中使用 JNDI 获得的ManagedExecutorService,为此替换了 CommonJ WorkManager。

7.1.2. 使用 TaskExecutor

Spring 的TaskExecutor实现用作简单的 JavaBean。在以下示例中,我们定义一个使用ThreadPoolTaskExecutor异步打印出一组消息的 bean:

import org.springframework.core.task.TaskExecutor;

public class TaskExecutorExample {

    private class MessagePrinterTask implements Runnable {

        private String message;

        public MessagePrinterTask(String message) {
            this.message = message;
        }

        public void run() {
            System.out.println(message);
        }
    }

    private TaskExecutor taskExecutor;

    public TaskExecutorExample(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    public void printMessages() {
        for(int i = 0; i < 25; i++) {
            taskExecutor.execute(new MessagePrinterTask("Message" + i));
        }
    }
}

如您所见,您不必将Runnable添加到队列中,而是从池中检索线程并自行执行。然后TaskExecutor使用其内部规则来决定何时执行任务。

要配置TaskExecutor使用的规则,我们公开了简单的 bean 属性:

<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="5"/>
    <property name="maxPoolSize" value="10"/>
    <property name="queueCapacity" value="25"/>
</bean>

<bean id="taskExecutorExample" class="TaskExecutorExample">
    <constructor-arg ref="taskExecutor"/>
</bean>

7.2. Spring TaskScheduler 抽象

除了TaskExecutor抽象之外,Spring 3.0 引入了TaskScheduler,它具有多种用于计划任务在将来某个 Moment 运行的方法。以下 Lists 显示了TaskScheduler接口定义:

public interface TaskScheduler {

    ScheduledFuture schedule(Runnable task, Trigger trigger);

    ScheduledFuture schedule(Runnable task, Instant startTime);

    ScheduledFuture schedule(Runnable task, Date startTime);

    ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);

    ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);

    ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);

    ScheduledFuture scheduleAtFixedRate(Runnable task, long period);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}

最简单的方法是名为schedule的方法,该方法只包含RunnableDate。这将导致任务在指定时间后运行一次。所有其他方法都可以安排任务重复运行。固定速率和固定延迟方法用于简单的定期执行,但是接受Trigger的方法更加灵活。

7.2.1. 触发界面

Trigger接口实质上受 JSR-236 的启发,该 JSR-236 在 Spring 3.0 之前尚未正式实现。 Trigger的基本思想是可以根据过去的执行结果甚至任意条件来确定执行时间。如果这些确定确实考虑了先前执行的结果,则该信息在TriggerContext内可用。 Trigger接口本身非常简单,如以下 Lists 所示:

public interface Trigger {

    Date nextExecutionTime(TriggerContext triggerContext);
}

TriggerContext是最重要的部分。它封装了所有相关数据,如有必要,将来可以扩展。 TriggerContext是一个接口(默认情况下使用SimpleTriggerContext实现)。以下 Lists 显示了Trigger实现的可用方法。

public interface TriggerContext {

    Date lastScheduledExecutionTime();

    Date lastActualExecutionTime();

    Date lastCompletionTime();
}

7.2.2. 触发实施

Spring 提供了Trigger接口的两种实现。最有趣的是CronTrigger。它启用了基于 cron 表达式的任务调度。例如,以下任务计划在每小时的 15 分钟后运行,但仅在工作日的 9 到 5 个“工作时间”内运行:

scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));

另一种实现是PeriodicTrigger,它接受固定的时间段,可选的初始延迟值和布尔值,以指示该时间段应被解释为固定速率还是固定延迟。由于TaskScheduler接口已经定义了用于以固定速率或固定延迟计划任务的方法,因此应尽可能直接使用这些方法。 PeriodicTrigger实现的价值在于您可以在依赖Trigger抽象的组件中使用它。例如,允许周期性触发器,基于 cron 的触发器,甚至自定义触发器实现可互换使用可能很方便。这样的组件可以利用依赖注入的优势,以便您可以在外部配置Triggers,因此可以轻松地对其进行修改或扩展。

7.2.3. TaskScheduler 的实现

像 Spring 的TaskExecutor抽象一样,TaskScheduler安排的主要好处是应用程序的调度需求与部署环境分离。当部署到不应由应用程序本身直接创建线程的应用程序服务器环境时,此抽象级别特别重要。对于这种情况,Spring 提供了TimerManagerTaskScheduler,它委派给 WebLogic 或 WebSphere 上的 CommonJ TimerManager,而最新的DefaultManagedTaskScheduler则委派给 Java EE 7 环境中的 JSR-236 ManagedScheduledExecutorService。两者通常都配置有 JNDI 查找。

每当不需要外部线程 Management 时,一个更简单的选择就是在应用程序中进行本地ScheduledExecutorService设置,可以通过 Spring 的ConcurrentTaskScheduler进行调整。为了方便起见,Spring 还提供了ThreadPoolTaskScheduler,它在内部委托ScheduledExecutorService来提供与ThreadPoolTaskExecutor相似的通用 bean 样式配置。这些变体也适用于宽松的应用程序服务器环境中的本地嵌入式线程池设置,尤其是在 Tomcat 和 Jetty 上。

7.3. 计划和异步执行的 Comments 支持

Spring 为任务调度和异步方法执行提供 Comments 支持。

7.3.1. 启用计划 Comments

要启用对@Scheduled@Async注解的支持,可以将@EnableScheduling@EnableAsync添加到您的@Configuration类中,如以下示例所示:

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

您可以选择与应用程序相关的 Comments。例如,如果只需要支持@Scheduled,则可以省略@EnableAsync。为了获得更细粒度的控制,您可以另外实现SchedulingConfigurer接口和/或AsyncConfigurer接口。有关完整的详细信息,请参见SchedulingConfigurerAsyncConfigurer javadoc。

如果您更喜欢 XML 配置,则可以使用<task:annotation-driven>元素,如以下示例所示:

<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>

请注意,使用前面的 XML,提供了执行程序引用来处理与带有@Async注解的方法相对应的那些任务,并且提供了调度程序引用来 Management 用@ScheduledComments 的那些方法。

Note

处理@Async注解的默认建议模式是proxy,该模式仅允许通过代理拦截呼叫。同一类内的本地调用无法以这种方式被拦截。对于更高级的侦听模式,请考虑结合编译时或加载时编织切换到aspectj模式。

7.3.2. @Scheduled 注解

您可以将@ScheduledComments 以及触发器元数据添加到方法中。例如,以下方法每隔五秒钟以固定的延迟被调用一次,这意味着该时间段是从每个先前调用的完成时间开始计算的:

@Scheduled(fixedDelay=5000)
public void doSomething() {
    // something that should execute periodically
}

如果需要固定汇率执行,则可以更改 Comments 中指定的属性名称。每五秒钟调用一次以下方法(在每次调用的连续开始时间之间测量):

@Scheduled(fixedRate=5000)
public void doSomething() {
    // something that should execute periodically
}

对于固定延迟和固定速率的任务,可以通过指示在第一次执行该方法之前要 await 的毫秒数来指定初始延迟,如以下fixedRate示例所示:

@Scheduled(initialDelay=1000, fixedRate=5000)
public void doSomething() {
    // something that should execute periodically
}

如果简单的定期调度不足以表现出来,则可以提供 cron 表达式。例如,以下仅在工作日执行:

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
    // something that should execute on weekdays only
}

Tip

您还可以使用zone属性指定解析 cron 表达式的时区。

请注意,要调度的方法必须具有空返回值,并且不能期望任何参数。如果该方法需要与应用程序上下文中的其他对象进行交互,则通常将通过依赖项注入来提供这些对象。

Note

从 Spring Framework 4.3 开始,任何范围的 bean 都支持@Scheduled方法。

确保不要在运行时初始化同一@ScheduledComments 类的多个实例,除非您确实希望为每个此类实例计划回调。与此相关,请确保不要在用@ScheduledComments 并已在容器中注册为常规 Spring Bean 的 bean 类上使用@Configurable。否则,您将获得双重初始化(一次通过容器,一次通过@Configurable方面),从而导致每个@Scheduled方法被调用两次。

7.3.3. @Async 注解

您可以在方法上提供@Async注解,以便异步调用该方法。换句话说,调用者在调用后立即返回,而方法的实际执行发生在已提交给 Spring TaskExecutor的任务中。在最简单的情况下,可以将 Comments 应用于返回void的方法,如以下示例所示:

@Async
void doSomething() {
    // this will be executed asynchronously
}

与用@ScheduledCommentsComments 的方法不同,这些方法可以使用参数,因为它们在运行时由调用方以“常规”方式调用,而不是从容器 Management 的计划任务中调用。例如,以下代码是@Async注解的合法应用:

@Async
void doSomething(String s) {
    // this will be executed asynchronously
}

即使返回值的方法也可以异步调用。但是,要求此类方法具有Future类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在对该Future调用get()之前执行其他任务。下面的示例演示如何在返回值的方法上使用@Async

@Async
Future<String> returnSomething(int i) {
    // this will be executed asynchronously
}

Tip

@Async方法不仅可以声明常规java.util.concurrent.Future返回类型,还可以声明 Spring 的org.springframework.util.concurrent.ListenableFuture,或者声明为 Spring 4.2 的 JDK 8 的java.util.concurrent.CompletableFuture,以与异步任务进行更丰富的交互并通过进一步的处理步骤立即进行组合。

您不能将@Async@PostConstruct之类的生命周期回调结合使用。要异步初始化 Spring Bean,当前必须使用一个单独的初始化 Spring Bean,然后在目标上调用@Async带 Comments 的方法,如以下示例所示:

public class SampleBeanImpl implements SampleBean {

    @Async
    void doSomething() {
        // ...
    }

}

public class SampleBeanInitializer {

    private final SampleBean bean;

    public SampleBeanInitializer(SampleBean bean) {
        this.bean = bean;
    }

    @PostConstruct
    public void initialize() {
        bean.doSomething();
    }

}

Note

@Async没有直接的 XML 等效项,因为此类方法应首先设计用于异步执行,而不是在外部重新声明为异步。但是,您可以结合使用自定义切入点,通过 Spring AOP 手动设置 Spring 的AsyncExecutionInterceptor

7.3.4. @Async 执行者资格

默认情况下,在方法上指定@Async时,使用的执行程序是在启用异步支持时配置,即,如果您使用的是 XML 或AsyncConfigurer实现(如果有),则为“Comments 驱动”元素。但是,当需要指示在执行给定方法时应使用默认值以外的 Actuator 时,可以使用@Async注解的value属性。以下示例显示了如何执行此操作:

@Async("otherExecutor")
void doSomething(String s) {
    // this will be executed asynchronously by "otherExecutor"
}

在这种情况下,"otherExecutor"可以是 Spring 容器中任何Executor bean 的名称,也可以是与任何Executor关联的限定符的名称(例如,由<qualifier>元素或 Spring 的@QualifierComments 指定)。

7.3.5. @Async 的异常 Management

@Async方法具有Future类型的返回值时,很容易 Management 在方法执行期间引发的异常,因为在Future结果上调用get时会引发此异常。但是,对于void返回类型,该异常未被捕获并且无法发送。您可以提供AsyncUncaughtExceptionHandler来处理此类异常。以下示例显示了如何执行此操作:

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        // handle exception
    }
}

默认情况下,仅记录异常。您可以使用AsyncConfigurer<task:annotation-driven/> XML 元素定义自定义AsyncUncaughtExceptionHandler

7.4. 任务命名空间

从 3.0 版开始,Spring 包含用于配置TaskExecutorTaskScheduler实例的 XML 名称空间。它还提供了一种方便的方法来配置要通过触发器安排的任务。

7.4.1. “调度程序”元素

以下元素创建具有指定线程池大小的ThreadPoolTaskScheduler实例:

<task:scheduler id="scheduler" pool-size="10"/>

id属性提供的值用作池中线程名称的前缀。 scheduler元素相对简单。如果不提供pool-size属性,则默认线程池只有一个线程。调度程序没有其他配置选项。

7.4.2. 执行者元素

下面创建一个ThreadPoolTaskExecutor实例:

<task:executor id="executor" pool-size="10"/>

previous section中显示的调度程序一样,为id属性提供的值用作池中线程名称的前缀。就池大小而言,executor元素比scheduler元素支持更多的配置选项。一方面,ThreadPoolTaskExecutor的线程池本身更具可配置性。执行者的线程池不仅可以具有单个大小,而且可以具有不同的核心和最大大小值。如果提供单个值,则执行程序具有固定大小的线程池(核心大小和最大大小相同)。但是,executor元素的pool-size属性也接受min-max形式的范围。下面的示例将5的最小值设置为25的最大值:

<task:executor
        id="executorWithPoolSizeRange"
        pool-size="5-25"
        queue-capacity="100"/>

在前面的配置中,还提供了queue-capacity值。还应根据执行者的队列容量来考虑线程池的配置。有关池大小和队列容量之间关系的完整描述,请参见ThreadPoolExecutor的文档。主要思想是,在提交任务时,如果活动线程数当前小于核心大小,则执行程序首先尝试使用空闲线程。如果已达到核心大小,则只要尚未达到其容量,就将任务添加到队列中。只有这样,如果达到队列的容量,执行程序才创建超出核心大小的新线程。如果还达到了最大大小,则执行者拒绝任务。

默认情况下,队列是无界的,但这很少是所需的配置,因为如果在所有池线程都忙时将足够的任务添加到该队列中,它将导致OutOfMemoryErrors。此外,如果队列是无界的,则最大大小完全无效。由于执行程序总是在创建超出核心大小的新线程之前尝试队列,因此队列必须具有有限的容量,线程池才能超出核心大小(这就是为什么固定大小的池是使用时唯一明智的情况无限队列)。

如上所述,考虑拒绝任务的情况。默认情况下,当任务被拒绝时,线程池执行程序将抛出TaskRejectedException。但是,拒绝策略实际上是可配置的。使用默认拒绝策略(即AbortPolicy实现)时会引发异常。对于在高负载下可以跳过某些任务的应用程序,您可以配置DiscardPolicyDiscardOldestPolicy。对于需要在重负载下限制提交的任务的应用程序,另一个很好的选择是CallerRunsPolicy。该策略不会引发异常或放弃任务,而是强制调用提交方法的线程运行任务本身。这个想法是这样的调用者在运行该任务时很忙,无法立即提交其他任务。因此,它提供了一种在保持线程池和队列限制的同时限制传入负载的简单方法。通常,这使执行程序可以“赶上”它正在处理的任务,从而释放队列,池中或两者中的某些容量。您可以从executor元素上rejection-policy属性可用值的枚举中选择任何一个。

下面的示例显示一个executor元素,该元素具有许多用于指定各种行为的属性:

<task:executor
        id="executorWithCallerRunsPolicy"
        pool-size="5-25"
        queue-capacity="100"
        rejection-policy="CALLER_RUNS"/>

最后,keep-alive设置确定线程终止之前可以保持空闲状态的时间限制(以秒为单位)。如果当前池中的线程数超过核心数,则在 await 此时间而不处理任务之后,多余的线程将被终止。时间值为零会导致多余的线程在执行任务后立即终止,而不会在任务队列中保留后续工作。下面的示例将keep-alive值设置为两分钟:

<task:executor
        id="executorWithKeepAlive"
        pool-size="5-25"
        keep-alive="120"/>

7.4.3. “计划任务”元素

Spring 任务名称空间最强大的功能是支持配置要在 Spring Application Context 中调度的任务。这遵循类似于 Spring 中其他“方法调用者”的方法,例如 JMS 命名空间提供的用于配置消息驱动的 POJO 的方法。基本上,ref属性可以指向任何 SpringManagement 的对象,而method属性提供要在该对象上调用的方法的名称。以下 Lists 显示了一个简单的示例:

<task:scheduled-tasks scheduler="myScheduler">
    <task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/>
</task:scheduled-tasks>

<task:scheduler id="myScheduler" pool-size="10"/>

调度程序由外部元素引用,并且每个单独的任务都包括其触发元数据的配置。在前面的示例中,该元数据定义了一个具有固定延迟的周期性触发器,该延迟指示了每个任务执行完成后要 await 的毫秒数。另一个选项是fixed-rate,指示应该执行该方法的频率,而不管以前的执行需要花费多长时间。此外,对于fixed-delayfixed-rate任务,您都可以指定一个“ initial-delay”参数,指示首次执行该方法之前要 await 的毫秒数。为了获得更多控制,您可以改为提供cron属性。以下示例显示了这些其他选项:

<task:scheduled-tasks scheduler="myScheduler">
    <task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/>
    <task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/>
    <task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>

<task:scheduler id="myScheduler" pool-size="10"/>

7.5. 使用 Quartz Scheduler

Quartz 使用TriggerJobJobDetail对象来实现各种作业的调度。有关 Quartz 的基本概念,请参见http://quartz-scheduler.org。为了方便起见,Spring 提供了两个类,这些类简化了在基于 Spring 的应用程序中使用 Quartz 的过程。

7.5.1. 使用 JobDetailFactoryBean

Quartz JobDetail对象包含运行作业所需的所有信息。 Spring 提供了JobDetailFactoryBean,它提供了用于 XML 配置目的的 bean 样式的属性。考虑以下示例:

<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
    <property name="jobClass" value="example.ExampleJob"/>
    <property name="jobDataAsMap">
        <map>
            <entry key="timeout" value="5"/>
        </map>
    </property>
</bean>

作业详细信息配置包含运行作业所需的所有信息(ExampleJob)。超时在作业数据 Map 中指定。作业数据 Map 可通过JobExecutionContext(在执行时传递给您)获得,但是JobDetail也会从 Map 到作业实例属性的作业数据中获取其属性。因此,在以下示例中,ExampleJob包含一个名为timeout的 bean 属性,并且JobDetail自动将其应用:

package example;

public class ExampleJob extends QuartzJobBean {

    private int timeout;

    /**
     * Setter called after the ExampleJob is instantiated
     * with the value from the JobDetailFactoryBean (5)
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
        // do the actual work
    }

}

您也可以使用作业数据 Map 中的所有其他属性。

Note

通过使用namegroup属性,您可以分别修改作业的名称和组。默认情况下,作业的名称与JobDetailFactoryBean(在上面的示例中为exampleJob)的 bean 名称匹配。

7.5.2. 使用 MethodInvokingJobDetailFactoryBean

通常,您只需要在特定对象上调用方法。通过使用MethodInvokingJobDetailFactoryBean,您可以完成此操作,如以下示例所示:

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="exampleBusinessObject"/>
    <property name="targetMethod" value="doIt"/>
</bean>

前面的示例导致在exampleBusinessObject方法上调用doIt方法,如以下示例所示:

public class ExampleBusinessObject {

    // properties and collaborators

    public void doIt() {
        // do the actual work
    }
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

通过使用MethodInvokingJobDetailFactoryBean,您无需创建仅调用方法的单行作业。您只需要创建实际的业务对象并连接详细对象即可。

缺省情况下,Quartz Jobs 是 Stateless 的,从而导致作业相互干扰的可能性。如果为相同的JobDetail指定两个触发器,则有可能在第一个作业完成之前启动第二个。如果JobDetail类实现Stateful接口,则不会发生。在第一个作业完成之前,第二个作业没有开始。要将MethodInvokingJobDetailFactoryBean产生的作业设为非并发,请将concurrent标志设置为false,如以下示例所示:

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="exampleBusinessObject"/>
    <property name="targetMethod" value="doIt"/>
    <property name="concurrent" value="false"/>
</bean>

Note

默认情况下,作业将以并发方式运行。

7.5.3. 通过使用触发器和 SchedulerFactoryBean 来连接作业

我们已经创建了工作详细信息和工作。我们还回顾了便捷 bean,该 bean 使您可以在特定对象上调用方法。当然,我们仍然需要自己安排工作。这是通过使用触发器和SchedulerFactoryBean完成的。 Quartz 中有几个触发器可用,Spring 提供了两个 Quartz FactoryBean实现,它们带有方便的默认值:CronTriggerFactoryBeanSimpleTriggerFactoryBean

触发器需要安排。 Spring 提供了一个SchedulerFactoryBean,它公开了要设置为属性的触发器。 SchedulerFactoryBean通过这些触发器计划实际的作业。

以下 Lists 同时使用了SimpleTriggerFactoryBeanCronTriggerFactoryBean

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
    <!-- see the example of method invoking job above -->
    <property name="jobDetail" ref="jobDetail"/>
    <!-- 10 seconds -->
    <property name="startDelay" value="10000"/>
    <!-- repeat every 50 seconds -->
    <property name="repeatInterval" value="50000"/>
</bean>

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
    <property name="jobDetail" ref="exampleJob"/>
    <!-- run every morning at 6 AM -->
    <property name="cronExpression" value="0 0 6 * * ?"/>
</bean>

前面的示例设置了两个触发器,一个触发器每隔 50 秒运行一次,启动延迟为 10 秒,另一个触发器每天清晨 6 点运行。要完成所有工作,我们需要设置SchedulerFactoryBean,如以下示例所示:

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="cronTrigger"/>
            <ref bean="simpleTrigger"/>
        </list>
    </property>
</bean>

SchedulerFactoryBean提供了更多属性,例如作业详细信息使用的 calendar,用于自定义 Quartz 的属性以及其他属性。有关更多信息,请参见SchedulerFactoryBean javadoc。

8.缓存抽象

从 3.1 版开始,Spring 框架提供了对将缓存透明添加到现有 Spring 应用程序的支持。与transaction支持类似,缓存抽象允许一致使用各种缓存解决方案,而对代码的影响最小。

从 Spring 4.1 开始,通过支持JSR-107 annotations和更多自定义选项,显着改善了缓存抽象。

8.1. 了解缓存抽象

缓存与缓冲区

术语“缓冲区”和“缓存”倾向于互换使用。但是请注意,它们代表不同的事物。传统上,缓冲区用作快速实体和慢速实体之间的数据的中间临时存储。由于一方必须 await 另一方(这会影响性能),因此缓冲区允许一次移动整个数据块,而不是一小段地移动数据块,从而缓解了这种情况。数据只能从缓冲区写入和读取一次。此外,缓冲区对于至少一个知道缓冲区的一方是可见的。

另一方面,根据定义,缓存是隐藏的,任何一方都不知道会发生缓存。它还可以提高性能,但是可以通过快速读取多次相同数据来实现。

您可以找到有关缓冲区和缓存here的区别的进一步说明。

缓存抽象的核心是将缓存应用于 Java 方法,从而根据缓存中可用的信息减少执行次数。也就是说,每次调用目标方法时,抽象都会应用一种缓存行为,该行为检查给定参数是否已经执行了该方法。如果已执行,则返回缓存的结果,而不必执行实际的方法。如果尚未执行该方法,则将执行该方法,并将结果缓存并返回给用户,以便下次调用该方法时,将返回缓存的结果。这样,对于给定的一组参数,昂贵的方法(无论是受 CPU 限制还是与 IO 绑定)只能执行一次,结果可以重复使用,而不必再次实际执行该方法。缓存逻辑是透明地应用的,不会对调用方造成任何干扰。

Tip

该方法仅适用于保证无论给定 Importing(或参数)执行多少次都返回相同输出(结果)的方法。

缓存抽象提供了其他与缓存相关的操作,例如更新缓存内容或删除一个或所有条目的能力。如果高速缓存处理在应用程序过程中可能更改的数据,则这些功能很有用。

与 Spring Framework 中的其他服务一样,缓存服务是一种抽象(不是缓存实现),并且需要使用实际的存储来存储缓存数据-也就是说,抽象使您不必编写缓存逻辑,但是没有提供实际的数据存储。 org.springframework.cache.Cacheorg.springframework.cache.CacheManager接口实现了这种抽象。

Spring 提供了这种抽象的一些实现:基于 JDK java.util.concurrent.ConcurrentMap的缓存,Ehcache 2.x,Gemfire 缓存,Caffeine和符合 JSR-107 的缓存(例如 Ehcache 3.x)。有关插入其他缓存存储区和提供程序的更多信息,请参见插入不同的后端缓存

Tip

对于多线程和多进程环境,缓存抽象没有特殊处理,因为此类功能由缓存实现处理。 。

如果您具有多进程环境(即,一个应用程序部署在多个节点上),则需要相应地配置缓存提供程序。根据您的用例,在几个节点上复制相同数据就足够了。但是,如果在应用程序过程中更改数据,则可能需要启用其他传播机制。

高速缓存特定项直接等同于通过程序化高速缓存交互找到的典型“如果找不到,然后 continue 处理并放入”代码块。没有应用锁,几个线程可能会尝试同时加载同一项目。驱逐同样如此。如果多个线程试图同时更新或逐出数据,则可以使用陈旧数据。某些缓存提供程序在该区域提供高级功能。有关更多详细信息,请参见缓存提供程序的文档。

要使用缓存抽象,您需要注意两个方面:

  • 缓存声明:确定需要缓存的方法及其策略。

  • 缓存配置:数据存储和读取的后备缓存。

8.2. 基于声明式 Comments 的缓存

对于缓存声明,Spring 的缓存抽象提供了一组 JavaComments:

  • @Cacheable:触发缓存填充。

  • @CacheEvict:触发缓存逐出。

  • @CachePut:在不影响方法执行的情况下更新缓存。

  • @Caching:重新组合要在一个方法上应用的多个缓存操作。

  • @CacheConfig:在类级别共享一些与缓存有关的常见设置。

8.2.1. @Cacheable 注解

顾名思义,您可以使用@Cacheable来划分可缓存的方法-即将结果存储在缓存中的方法,以便在后续调用(具有相同参数)时返回缓存中的值,而不会必须实际执行该方法。Comments 声明以最简单的形式要求与带 Comments 的方法关联的缓存名称,如以下示例所示:

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

在前面的代码段中,findBook方法与名为books的缓存关联。每次调用该方法时,都会检查缓存以查看调用是否已执行并且不必重复。虽然在大多数情况下,仅声明一个缓存,但是 Comments 允许指定多个名称,以便使用多个缓存。在这种情况下,在执行方法之前检查每个缓存-如果命中了至少一个缓存,则返回关联的值。

Note

即使未实际执行缓存的方法,所有其他不包含该值的缓存也会被更新。

以下示例在findBook方法上使用@Cacheable

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
默认密钥生成

由于缓存本质上是键值存储,因此每次调用缓存的方法都需要转换为适合缓存访问的键。缓存抽象使用基于以下算法的简单KeyGenerator

  • 如果没有给出参数,则返回SimpleKey.EMPTY

  • 如果仅给出一个参数,则返回该实例。

  • 如果给出了一个以上的参数,则返回一个包含所有参数的SimpleKey

只要参数具有自然键并实现有效的hashCode()equals()方法,该方法就适用于大多数用例。如果不是这种情况,则需要更改策略。

要提供其他默认密钥生成器,您需要实现org.springframework.cache.interceptor.KeyGenerator接口。

Note

随着 Spring 4.0 的发布,默认的密钥生成策略发生了变化。 Spring 的早期版本使用密钥生成策略,该策略对于多个关键参数仅考虑参数hashCode()而不考虑equals()。这可能会导致意外的按键冲突(有关背景,请参见SPR-10237)。对于这种情况,新的SimpleKeyGenerator使用复合键。

如果要 continue 使用以前的关键策略,则可以配置已弃用的org.springframework.cache.interceptor.DefaultKeyGenerator类或创建基于哈希的自定义KeyGenerator实现。

自定义密钥生成声明

由于缓存是通用的,因此目标方法很可能具有各种签名,这些签名无法轻易 Map 到缓存结构的顶部。当目标方法具有多个参数时,只有其中一些参数适合缓存(而其余参数仅由方法逻辑使用),这往往会变得很明显。考虑以下示例:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

乍一看,虽然两个boolean参数会影响书的查找方式,但它们对缓存没有用处。此外,如果两者中只有一个重要而另一个不重要怎么办?

在这种情况下,使用@Cacheable注解可以指定如何通过其key属性生成密钥。您可以使用SpEL来选择感兴趣的参数(或其嵌套属性),执行操作甚至调用任意方法,而无需编写任何代码或实现任何接口。这是在default generator上推荐的方法,因为随着代码库的增长,方法的签名趋向于完全不同。虽然默认策略可能适用于某些方法,但很少适用于所有方法。

以下示例是各种 SpEL 声明(如果您不熟悉 SpEL,请帮忙并阅读Spring 表达语言):

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

前面的代码片段显示了选择某个参数,其属性之一甚至是任意(静态)方法是多么容易。

如果负责生成密钥的算法过于具体或需要共享,则可以在操作上定义一个自定义keyGenerator。为此,请指定要使用的KeyGenerator bean 实现的名称,如以下示例所示:

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

Note

keykeyGenerator参数是互斥的,并且同时指定这两个参数的操作将导致异常。

默认缓存分辨率

缓存抽象使用简单的CacheResolver,该CacheResolver通过使用配置的CacheManager检索在操作级别定义的缓存。

要提供其他默认缓存解析器,您需要实现org.springframework.cache.interceptor.CacheResolver接口。

自定义缓存解析

默认的缓存分辨率非常适合使用单个CacheManager并且没有复杂的缓存分辨率要求的应用程序。

对于使用多个缓存 Management 器的应用程序,可以将cacheManager设置为用于每个操作,如以下示例所示:

@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1)
public Book findBook(ISBN isbn) {...}
  • (1) 指定anotherCacheManager

您还可以完全以替换key generation的方式替换CacheResolver。对于每个缓存操作,都要求解决该问题,让实现实际上可以根据运行时参数来解析要使用的缓存。以下示例显示如何指定CacheResolver

@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
  • (1) 指定CacheResolver

Note

从 Spring 4.1 开始,缓存 Comments 的value属性不再是必需的,因为CacheResolver可以提供此特定信息,而不管 Comments 的内容如何。

keykeyGenerator类似,cacheManagercacheResolver参数是互斥的,并且同时指定这两个参数的操作将导致异常。因为CacheResolver实现会忽略自定义CacheManager。这可能不是您所期望的。

Synchronized Caching

在多线程环境中,可能会为同一参数同时调用某些操作(通常是在启动时)。默认情况下,缓存抽象不会锁定任何内容,并且可能会多次计算相同的值,从而破坏了缓存的目的。

对于那些特殊情况,您可以使用sync属性来指示基础缓存提供程序在计算值时锁定缓存条目。结果,只有一个线程正在忙于计算该值,而其他线程则被阻塞,直到在缓存中更新该条目为止。下面的示例演示如何使用sync属性:

@Cacheable(cacheNames="foos", sync=true) (1)
public Foo executeExpensiveOperation(String id) {...}
  • (1) 使用sync属性。

Note

这是一项可选功能,您最喜欢的缓存库可能不支持它。核心框架提供的所有CacheManager实现都支持它。有关更多详细信息,请参见缓存提供程序的文档。

Conditional Caching

有时,一种方法可能并不总是适合缓存(例如,它可能取决于给定的参数)。高速缓存注解通过condition参数支持此类功能,该参数采用SpEL表达式,该表达式被评估为truefalse。如果为true,则将缓存该方法。如果不是,它的行为就好像未缓存该方法一样(也就是说,无论缓存中使用什么值或使用了什么参数,每次都会执行该方法)。例如,仅当参数name的长度短于 32 时,才缓存以下方法:

@Cacheable(cacheNames="book", condition="#name.length() < 32") (1)
public Book findBook(String name)
  • (1)@Cacheable上设置条件。

除了condition参数之外,您还可以使用unless参数来否决将值添加到缓存中。与condition不同,unless表达式是在调用方法之后求值的。为了扩展前面的示例,也许我们只想缓存平装书,如以下示例所示:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1)
public Book findBook(String name)
  • (1) 使用unless属性来阻止精装书。

缓存抽象支持java.util.Optional,仅当其存在时才将其内容用作缓存值。 #result始终引用业务实体,而不引用受支持的包装器,因此可以将以下示例重写为:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)

请注意,result仍指Book而不是Optional。因为可能是null,所以我们应该使用安全的导航操作符。

可用的缓存 SpEL 评估上下文

每个SpEL表达式针对专用的context求值。除了内置参数外,该框架还提供了与缓存相关的专用元数据,例如参数名称。下表描述了可用于上下文的项目,以便您可以将其用于键和条件计算:

表 11. Cache SpEL 可用元数据

NameLocationDescriptionExample
methodNameRoot object被调用方法的名称#root.methodName
methodRoot object被调用的方法#root.method.name
targetRoot object被调用的目标对象#root.target
targetClassRoot object被调用目标的类#root.targetClass
argsRoot object用于调用目标的参数(作为数组)#root.args[0]
cachesRoot object执行当前方法所依据的缓存集合#root.caches[0].name
Argument nameEvaluation context任何方法参数的名称。如果名称不可用(可能是由于没有调试信息),则参数名称在#a<#arg>下也可用,其中#arg代表参数索引(从0开始)。#iban#a0(您也可以使用#p0#p<#arg>表示法作为别名)。
resultEvaluation context方法调用的结果(要缓存的值)。仅在unless个表达式,cache put个表达式(用于计算key)或cache evict个表达式(当beforeInvocationfalse时)中可用。对于受支持的包装器(例如Optional),#result指的是实际对象,而不是包装器。#result

8.2.2. @CachePut 注解

当需要在不影响方法执行的情况下更新缓存时,可以使用@CachePut注解。也就是说,该方法始终执行,并将其结果放入缓存中(根据@CachePut选项)。它支持与@Cacheable相同的选项,应用于缓存填充,而不是方法流优化。以下示例使用@CachePut注解:

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)

Tip

强烈建议不要在同一方法上使用@CachePut@CacheableComments,因为它们具有不同的行为。后者导致通过使用缓存跳过方法执行,而前者则强制执行以便执行缓存更新。这会导致意外的行为,并且,除了特定的极端情况(例如具有相互排斥条件的 Comments)外,应避免此类声明。还请注意,此类条件不应依赖于结果对象(即#result变量),因为这些条件已预先验证以确认排除。

8.2.3. @CacheEvict 注解

缓存抽象不仅允许缓存存储的填充,还允许逐出。此过程对于从缓存中删除陈旧或未使用的数据很有用。与@Cacheable相反,@CacheEvict划分了执行高速缓存逐出的方法(即,用作触发从高速缓存中删除数据的触发器的方法)。与其同级类似,@CacheEvict要求指定一个或多个受操作影响的缓存,允许指定自定义缓存和键解析或条件,并具有一个额外的参数(allEntries),该参数指示是否需要在整个缓存范围内逐出而不是只是逐项驱逐(基于密钥)。下面的示例从books缓存中逐出所有条目:

@CacheEvict(cacheNames="books", allEntries=true) (1)
public void loadBooks(InputStream batch)
  • (1) 使用allEntries属性从缓存中逐出所有条目。

当需要清除整个缓存区域时,此选项非常有用。而不是逐出每个条目(由于效率低下,这将花费很长时间),因此一次操作会删除所有条目,如前面的示例所示。请注意,该框架会忽略此方案中指定的任何键,因为它不适用(整个高速缓存被驱逐,而不仅仅是一个条目)。

您还可以使用beforeInvocation属性指示驱逐应该发生在(默认之后)还是方法执行之前。前者提供与其余注解相同的语义:方法成功完成后,将对缓存执行操作(在这种情况下为逐出)。如果该方法未执行(可能已被缓存)或引发了异常,则不会发生逐出。后者(beforeInvocation=true)导致逐出总是在调用该方法之前发生。在不需要将逐出与方法结果联系在一起的情况下,这很有用。

请注意,void方法可与@CacheEvict一起使用-由于这些方法充当触发器,因此将忽略返回值(因为它们不会与缓存交互)。 @Cacheable并非如此,@Cacheable向缓存中添加或更新数据,因此需要结果。

8.2.4. @Caching 注解

有时,需要指定多个相同类型的 Comments(例如@CacheEvict@CachePut),例如,因为不同缓存之间的条件或键表达式不同。 @Caching允许在同一方法上使用多个嵌套的@Cacheable@CachePut@CacheEvict注解。以下示例使用两个@CacheEvictComments:

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

8.2.5. @CacheConfig 注解

到目前为止,我们已经看到缓存操作提供了许多自定义选项,并且您可以为每个操作设置这些选项。但是,如果某些自定义选项适用于该类的所有操作,则配置它们可能很繁琐。例如,指定用于类的每个高速缓存操作的高速缓存的名称可以由单个类级定义代替。这是@CacheConfig发挥作用的地方。以下示例使用@CacheConfig设置缓存的名称:

@CacheConfig("books") (1)
public class BookRepositoryImpl implements BookRepository {

    @Cacheable
    public Book findBook(ISBN isbn) {...}
}
  • (1) 使用@CacheConfig设置缓存名称。

@CacheConfig是类级别的注解,它允许共享缓存名称,自定义KeyGenerator,自定义CacheManager和自定义CacheResolver。将此 Comments 放在类上不会打开任何缓存操作。

操作级别的自定义始终会覆盖在@CacheConfig上设置的自定义。因此,这为每个缓存操作提供了三个定制级别:

  • 全局配置,可用于CacheManagerKeyGenerator

  • 在 Class 上,使用@CacheConfig

  • 在操作级别。

8.2.6. 启用缓存 Comments

重要的是要注意,即使声明缓存 Comments 并不会自动触发它们的动作-就像 Spring 中的许多事情一样,必须声明性地启用该功能(这意味着如果您怀疑应该归因于缓存,则可以通过删除来禁用它仅一个配置行,而不是代码中的所有 Comments)。

要启用缓存 Comments,请将 Comments@EnableCaching添加到您的@Configuration类中:

@Configuration
@EnableCaching
public class AppConfig {
}

另外,对于 XML 配置,您可以使用cache:annotation-driven元素:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

        <cache:annotation-driven/>
</beans>

cache:annotation-driven元素和@EnableCachingComments 都允许您指定各种选项,这些选项影响通过 AOP 将缓存行为添加到应用程序的方式。该配置有意与@Transactional相似。

Note

处理缓存 Comments 的默认建议模式为proxy,该模式仅允许通过代理拦截呼叫。同一类内的本地调用无法以这种方式被拦截。对于更高级的拦截模式,请考虑结合编译时或加载时编织切换到aspectj模式。

Note

有关实现CachingConfigurer所需的高级自定义(使用 Java 配置)的更多详细信息,请参见javadoc

表 12.缓存 Comments 设置

XML AttributeAnnotation AttributeDefaultDescription
cache-manager不适用(请参见CachingConfigurer javadoc)cacheManager要使用的缓存 Management 器的名称。使用此缓存 Management 器在后台初始化默认值CacheResolver(如果未设置,则初始化cacheManager)。为了更精细地 Management 缓存分辨率,请考虑设置“ cache-resolver”属性。
cache-resolver不适用(请参见CachingConfigurer javadoc)使用已配置的cacheManagerSimpleCacheResolver用来解析后备缓存的 CacheResolver 的 bean 名称。此属性不是必需的,仅需指定为“ cache-manager”属性的替代方法。
key-generator不适用(请参见CachingConfigurer javadoc)SimpleKeyGenerator要使用的定制密钥生成器的名称。
error-handler不适用(请参见CachingConfigurer javadoc)SimpleCacheErrorHandler要使用的自定义缓存错误处理程序的名称。默认情况下,在与缓存相关的操作期间抛出的所有异常都将返回给 Client 端。
modemodeproxy缺省模式(proxy)使用 Spring 的 AOP 框架处理要 Comments 的 bean(遵循代理语义,如前所述,仅适用于通过代理传入的方法调用)。相反,替代模式(aspectj)使用 Spring 的 AspectJ 缓存方面编织受影响的类,修改目标类字节码以应用于任何类型的方法调用。 AspectJ 编织需要在 Classpath 中使用spring-aspects.jar并启用加载时编织(或编译时编织)。 (有关如何设置加载时编织的详细信息,请参见Spring configuration。)
proxy-target-classproxyTargetClassfalse仅适用于代理模式。控制为使用@Cacheable@CacheEvictCommentsComments 的类创建哪种类型的缓存代理。如果proxy-target-class属性设置为true,则会创建基于类的代理。如果proxy-target-classfalse或省略了属性,则将创建基于标准 JDK 接口的代理。 (有关不同代理类型的详细检查,请参见Proxying Mechanisms。)
orderorderOrdered.LOWEST_PRECEDENCE定义应用于带@Cacheable@CacheEvictComments 的 bean 的缓存建议的 Sequences。 (有关与 OrderAOP 通知有关的规则的更多信息,请参阅Advice Ordering。)没有指定的排序意味着 AOP 子系统确定建议的 Sequences。

Note

<cache:annotation-driven/>仅在定义它的相同应用程序上下文中的 Bean 上寻找@Cacheable/@CachePut/@CacheEvict/@Caching。这意味着,如果将<cache:annotation-driven/>放在WebApplicationContext中,而将DispatcherServlet放在_5 中,则它仅在控制器中检查 bean,而不在服务中检查 bean。有关更多信息,请参见MVC 部分

方法可见性和缓存 Comments

使用代理时,应仅将缓存 Comments 应用于具有公共可见性的方法。如果使用这些 Comments 对受保护的,私有的或程序包可见的方法进行 Comments,则不会引发任何错误,但是带 Comments 的方法不会显示已配置的缓存设置。如果您需要 Comments 非公共方法,请考虑使用 AspectJ(请参阅本节的其余部分),因为它会更改字节码本身。

Tip

Spring 建议您仅使用@Cache*Comments 对具体类(以及具体类的方法)进行 Comments,而不是对接口进行 Comments。您当然可以在接口(或接口方法)上放置@Cache*注解,但这仅在您使用基于接口的代理时才可以预期地起作用。 Java 注解不是从接口继承的事实意味着,如果您使用基于类的代理(proxy-target-class="true")或基于编织的方面(mode="aspectj"),则代理和编织基础结构无法识别缓存设置,并且该对象是没有包装在缓存代理中,那肯定是不好的。

Note

在代理模式(默认)下,仅拦截通过代理传入的外部方法调用。这意味着即使调用的方法标记为@Cacheable,自调用(实际上是目标对象内的方法调用目标对象的另一种方法)也不会导致实际的缓存。在这种情况下,请考虑使用aspectj模式。另外,必须完全初始化代理以提供预期的行为,因此您不应在初始化代码(即@PostConstruct)中依赖此功能。

8.2.7. 使用自定义 Comments

自定义 Comments 和 AspectJ

该功能仅适用于基于代理的方法,但可以通过使用 AspectJ 进行一些额外的工作来启用。

spring-aspects模块仅为标准 Comments 定义一个方面。如果定义了自己的 Comments,则还需要为其定义一个方面。查看AnnotationCacheAspect为例。

缓存抽象使您可以使用自己的 Comments 来标识哪种方法触发缓存填充或逐出。作为模板机制,这非常方便,因为它消除了重复缓存注解声明的需求,如果指定了键或条件或代码库中不允许外部导入(org.springframework),则这特别有用。与其余stereotype注解类似,可以将@Cacheable@CachePut@CacheEvict@CacheConfig用作meta-annotations(即可以 Comments 其他注解的注解)。在以下示例中,我们用自己的自定义 Comments 替换了通用的@Cacheable声明:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}

在前面的示例中,我们定义了自己的SlowServiceComments,该 Comments 本身被@CacheableComments。现在我们可以替换以下代码:

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

以下示例显示了自定义注解,我们可以用其替换前面的代码:

@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

即使@SlowService不是 SpringComments,容器也会在运行时自动获取其声明并理解其含义。请注意,如earlier所述,需要启用 Comments 驱动的行为。

8.3. JCache(JSR-107)注解

从 4.1 版开始,缓存抽象完全支持 JCache 标准 Comments:@CacheResult@CachePut@CacheRemove@CacheRemoveAll以及@CacheDefaults@CacheKey@CacheValue随播。您可以使用这些 Comments,而无需将缓存存储迁移到 JSR-107.内部实现使用 Spring 的缓存抽象,并提供符合规范的默认CacheResolverKeyGenerator实现。换句话说,如果您已经在使用 Spring 的缓存抽象,则可以切换到这些标准 Comments,而无需更改缓存存储(或配置)。

8.3.1. 功能摘要

对于那些熟悉 Spring 缓存 Comments 的人,下表描述了 SpringComments 与 JSR-107 副本之间的主要区别:

表 13. Spring 和 JSR-107 缓存 Comments

SpringJSR-107Remark
@Cacheable@CacheResult相当相似。 @CacheResult可以缓存特定的异常并强制执行该方法,而不管缓存的内容如何。
@CachePut@CachePut当 Spring 使用方法调用的结果更新缓存时,JCache 要求将其作为参数传递给@CacheValue。由于存在这种差异,JCache 允许在实际方法调用之前或之后更新缓存。
@CacheEvict@CacheRemove相当相似。当方法调用导致异常时,@CacheRemove支持条件驱逐。
@CacheEvict(allEntries=true)@CacheRemoveAll参见@CacheRemove
@CacheConfig@CacheDefaults让您以类似的方式配置相同的概念。

JCache 具有javax.cache.annotation.CacheResolver的概念,该概念与 Spring 的CacheResolver接口相同,只是 JCache 仅支持单个缓存。默认情况下,一个简单的实现根据 Comments 中声明的名称检索要使用的缓存。应该注意的是,如果 Comments 中未指定缓存名称,则会自动生成一个默认值。有关更多信息,请参见@CacheResult#cacheName()的 javadoc。

CacheResolver实例由CacheResolverFactory检索。可以为每个缓存操作自定义工厂,如以下示例所示:

@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class) (1)
public Book findBook(ISBN isbn)
  • (1) 为此操作定制工厂。

Note

对于所有引用的类,Spring 尝试查找具有给定类型的 bean。如果存在多个匹配项,那么将创建一个新实例,并可以使用常规 bean 生命周期回调,例如依赖项注入。

密钥是由javax.cache.annotation.CacheKeyGenerator生成的,其目的与 Spring 的KeyGenerator相同。默认情况下,将考虑所有方法参数,除非至少一个参数用@CacheKeyComments。这类似于 Spring 的自定义密钥生成声明。例如,以下是相同的操作,一个使用 Spring 的抽象,另一个使用 JCache:

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@CacheResult(cacheName="books")
public Book findBook(@CacheKey ISBN isbn, boolean checkWarehouse, boolean includeUsed)

您还可以在操作上指定CacheKeyResolver,类似于指定CacheResolverFactory的方式。

JCache 可以 Management 带 Comments 的方法引发的异常。这样可以防止更新缓存,但也可以将异常缓存为失败的指示,而不必再次调用该方法。假设如果 ISBN 的结构无效,则抛出InvalidIsbnNotFoundException。这是一个永久性的失败(使用这样的参数无法检索任何书籍)。以下内容缓存了该异常,以便使用相同的无效 ISBN 进行的进一步调用直接引发该缓存的异常,而不是再次调用该方法:

@CacheResult(cacheName="books", exceptionCacheName="failures"
            cachedExceptions = InvalidIsbnNotFoundException.class)
public Book findBook(ISBN isbn)

8.3.2. 启用 JSR-107 支持

除了启用 Spring 的声明性 Comments 支持外,您无需执行任何其他操作即可启用 JSR-107 支持。如果 Classpath 中同时存在 JSR-107 API 和spring-context-support模块,则@EnableCachingcache:annotation-driven元素都会自动启用 JCache 支持。

Note

根据您的用例,选择基本上是您的选择。您甚至可以通过在某些服务器上使用 JSR-107 API 并在其他服务器上使用 Spring 自己的 Comments 来混合和匹配服务。但是,如果这些服务影响相同的缓存,则应使用一致且相同的密钥生成实现。

8.4. 基于声明式 XML 的缓存

如果不能使用 Comments(可能是由于无法访问源代码或没有外部代码),则可以使用 XML 进行声明式缓存。因此,您可以在外部指定目标方法和缓存指令,而不是 Comments 用于缓存的方法(类似于声明式事务 Managementadvice)。上一节中的示例可以转换为以下示例:

<!-- the service we want to make cacheable -->
<bean id="bookService" class="x.y.service.DefaultBookService"/>

<!-- cache definitions -->
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
    <cache:caching cache="books">
        <cache:cacheable method="findBook" key="#isbn"/>
        <cache:cache-evict method="loadBooks" all-entries="true"/>
    </cache:caching>
</cache:advice>

<!-- apply the cacheable behavior to all BookService interfaces -->
<aop:config>
    <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>

<!-- cache manager definition omitted -->

在前面的配置中,bookService已设置为可缓存。要应用的缓存语义封装在cache:advice定义中,这将导致findBooks方法用于将数据放入缓存,而loadBooks方法用于将数据逐出。两种定义都针对books缓存。

aop:config定义通过使用 AspectJ 切入点表达式将缓存建议应用于程序中的适当点(更多信息可在Spring 面向方面的编程中获得)。在前面的示例中,考虑了BookService中的所有方法,并将缓存建议应用于它们。

声明式 XML 缓存支持所有基于 Comments 的模型,因此在两者之间移动应该相当容易。此外,两者都可以在同一应用程序内使用。基于 XML 的方法不会涉及目标代码。但是,它本质上比较冗长。当处理具有用于缓存的重载方法的类时,确定合适的方法确实需要付出额外的努力,因为method参数不是很好的判别器。在这些情况下,您可以使用 AspectJ 切入点来挑选目标方法并应用适当的缓存功能。但是,通过 XML,更容易应用程序包或组或接口范围的缓存(同样,由于 AspectJ 切入点的缘故)和创建类似模板的定义(就像我们在前面的示例中一样,通过cache:definitions定义了目标缓存) cache属性)。

8.5. 配置缓存存储

缓存抽象提供了几种存储集成选项。要使用它们,您需要声明一个适当的CacheManager(控制和 ManagementCache实例的实体,该实体可用于检索这些实例以进行存储)。

8.5.1. 基于 JDK ConcurrentMap 的缓存

基于 JDK 的Cache实现位于org.springframework.cache.concurrent包下。它使您可以将ConcurrentHashMap用作后备Cache存储。以下示例显示了如何配置两个缓存:

<!-- simple cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
    <property name="caches">
        <set>
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
        </set>
    </property>
</bean>

前面的代码段使用SimpleCacheManager为名为defaultbooks的两个嵌套ConcurrentMapCache实例创建CacheManager。请注意,名称是直接为每个缓存配置的。

由于缓存是由应用程序创建的,因此绑定到其生命周期,使其适合于基本用例,测试或简单的应用程序。缓存可以很好地扩展并且非常快,但是它不提供任何 Management,持久性功能或驱逐 Contract。

8.5.2. 基于 Ehcache 的缓存

Note

Ehcache 3.x 完全符合 JSR-107,并且不需要专用支持。

Ehcache 2.x 实现位于org.springframework.cache.ehcache包中。同样,要使用它,您需要声明适当的CacheManager。以下示例显示了如何执行此操作:

<bean id="cacheManager"
        class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>

<!-- EhCache library setup -->
<bean id="ehcache"
        class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>

此设置在 Spring IoC 内部引导 ehcache 库(通过ehcache bean),然后将其连接到专用的CacheManager实现中。请注意,完整的 ehcache 特定配置是从ehcache.xml读取的。

8.5.3. Caffeine 缓存

Caffeine 是对 Guava 缓存的 Java 8 重写,其实现位于org.springframework.cache.caffeine包中,并提供对 Caffeine 多个功能的访问。

下面的示例配置一个CacheManager来按需创建缓存:

<bean id="cacheManager"
        class="org.springframework.cache.caffeine.CaffeineCacheManager"/>

您还可以提供要显式使用的缓存。在这种情况下,Manager 只能提供那些。以下示例显示了如何执行此操作:

<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
    <property name="caches">
        <set>
            <value>default</value>
            <value>books</value>
        </set>
    </property>
</bean>

CaffeineCacheManager还支持自定义CaffeineCacheLoader。有关这些的更多信息,请参见Caffeine documentation

8.5.4. 基于 GemFire 的缓存

GemFire 是面向内存,磁盘支持,弹性可伸缩,连续可用,活动(具有内置的基于模式的订阅通知),全局复制的数据库,并提供功能齐全的边缘缓存。有关如何将 GemFire 用作CacheManager(以及更多)的更多信息,请参见Spring Data GemFire 参考文档

8.5.5. JSR-107 缓存

Spring 的缓存抽象也可以使用符合 JSR-107 的缓存。 JCache 实现位于org.springframework.cache.jcache包中。

同样,要使用它,您需要声明适当的CacheManager。以下示例显示了如何执行此操作:

<bean id="cacheManager"
        class="org.springframework.cache.jcache.JCacheCacheManager"
        p:cache-manager-ref="jCacheManager"/>

<!-- JSR-107 cache manager setup  -->
<bean id="jCacheManager" .../>

8.5.6. 在没有后备存储的情况下处理缓存

有时,在切换环境或进行测试时,您可能具有缓存声明而未配置实际的后备缓存。由于这是无效的配置,因此在运行时会引发异常,因为缓存基础结构无法找到合适的存储。在这种情况下,可以删除简单的伪高速缓存,而不执行高速缓存,而不是删除高速缓存声明(这可能很乏味),即不强制执行高速缓存的方法,即每次都执行高速缓存的方法。以下示例显示了如何执行此操作:

<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
    <property name="cacheManagers">
        <list>
            <ref bean="jdkCache"/>
            <ref bean="gemfireCache"/>
        </list>
    </property>
    <property name="fallbackToNoOpCache" value="true"/>
</bean>

前面的CompositeCacheManager链接了多个CacheManager实例,并通过fallbackToNoOpCache标志为未配置的缓存 Management 器处理的所有定义添加了一个无操作缓存。也就是说,在jdkCachegemfireCache中未找到的每个缓存定义(在示例中较早配置)均由不存储任何信息的无操作缓存处理,导致每次执行目标方法。

8.6. 插入不同的后端缓存

显然,有很多缓存产品可以用作后备存储。要插入它们,您需要提供CacheManagerCache的实现,因为不幸的是,没有可用的替代标准。这听起来可能比实际要难,因为在实践中,这些类往往是简单的adapters,就像ehcache类一样,它们将缓存抽象框架 Map 到存储 API 的顶部。大多数CacheManager类可以使用org.springframework.cache.support包中的类(例如AbstractCacheManager,它负责样板代码,仅保留实际的 Map)。我们希望,及时提供与 Spring 集成的库可以弥补这一小的配置空白。

8.7. 如何设置 TTL/TTI /驱逐策略/ XXX 功能?

直接通过您的缓存提供程序。缓存抽象是一种抽象,而不是缓存实现。您使用的解决方案可能支持其他解决方案不支持的各种数据策略和不同的拓扑(例如,JDK ConcurrentHashMap -在缓存抽象中暴露出来将是无用的,因为没有后备支持)。此类功能应通过后备缓存(配置时)或通过其本机 API 直接控制。

9. Appendix

9.1. XML 模式

附录的此部分列出了与集成技术有关的 XML 模式。

9.1.1. jee 模式

jee元素处理与 Java EE(Java 企业版)配置有关的问题,例如查找 JNDI 对象和定义 EJB 引用。

要使用jee模式中的元素,您需要在 Spring XML 配置文件的顶部具有以下序言。以下代码段中的文本引用了正确的架构,以便您可以使用jee名称空间中的元素:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">

    <!-- bean definitions here -->
</beans>
<jee:jndi-lookup/> (simple)

下面的示例显示如何使用 JNDI 查找没有jee模式的数据源:

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="jdbc/MyDataSource"/>
</bean>
<bean id="userDao" class="com.foo.JdbcUserDao">
    <!-- Spring will do the cast automatically (as usual) -->
    <property name="dataSource" ref="dataSource"/>
</bean>

下面的示例演示如何使用 JNDI 来通过jee模式查找数据源:

<jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource"/>

<bean id="userDao" class="com.foo.JdbcUserDao">
    <!-- Spring will do the cast automatically (as usual) -->
    <property name="dataSource" ref="dataSource"/>
</bean>
<jee:jndi-lookup/>(具有单个 JNDI 环境设置)

下面的示例演示如何使用 JNDI 查找不带jee的环境变量:

<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="jdbc/MyDataSource"/>
    <property name="jndiEnvironment">
        <props>
            <prop key="ping">pong</prop>
        </props>
    </property>
</bean>

下面的示例演示如何使用 JNDI 通过jee查找环境变量:

<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
    <jee:environment>ping=pong</jee:environment>
</jee:jndi-lookup>
<jee:jndi-lookup/>(具有多个 JNDI 环境设置)

下面的示例演示如何使用 JNDI 查找不带jee的多个环境变量:

<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="jdbc/MyDataSource"/>
    <property name="jndiEnvironment">
        <props>
            <prop key="sing">song</prop>
            <prop key="ping">pong</prop>
        </props>
    </property>
</bean>

以下示例显示如何使用 JNDI 通过jee查找多个环境变量:

<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
    <!-- newline-separated, key-value pairs for the environment (standard Properties format) -->
    <jee:environment>
        sing=song
        ping=pong
    </jee:environment>
</jee:jndi-lookup>
<jee:jndi-lookup/> (Complex)

下面的示例演示如何使用 JNDI 查找数据源以及许多没有jee的不同属性:

<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="jdbc/MyDataSource"/>
    <property name="cache" value="true"/>
    <property name="resourceRef" value="true"/>
    <property name="lookupOnStartup" value="false"/>
    <property name="expectedType" value="com.myapp.DefaultThing"/>
    <property name="proxyInterface" value="com.myapp.Thing"/>
</bean>

下面的示例演示如何使用 JNDI 使用jee查找数据源和许多不同的属性:

<jee:jndi-lookup id="simple"
        jndi-name="jdbc/MyDataSource"
        cache="true"
        resource-ref="true"
        lookup-on-startup="false"
        expected-type="com.myapp.DefaultThing"
        proxy-interface="com.myapp.Thing"/>
<jee:local-slsb/> (Simple)

<jee:local-slsb/>元素配置对本地 EJB Stateless SessionBean 的引用。

以下示例显示如何配置对不带jee的本地 EJB Stateless SessionBean 的引用:

<bean id="simple"
        class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/RentalServiceBean"/>
    <property name="businessInterface" value="com.foo.service.RentalService"/>
</bean>

以下示例显示如何使用jee配置对本地 EJB Stateless SessionBean 的引用:

<jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean"
        business-interface="com.foo.service.RentalService"/>
<jee:local-slsb/> (Complex)

<jee:local-slsb/>元素配置对本地 EJB Stateless SessionBean 的引用。

以下示例说明如何配置对本地 EJB Stateless SessionBean 的引用以及许多不带有jee的属性:

<bean id="complexLocalEjb"
        class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/RentalServiceBean"/>
    <property name="businessInterface" value="com.example.service.RentalService"/>
    <property name="cacheHome" value="true"/>
    <property name="lookupHomeOnStartup" value="true"/>
    <property name="resourceRef" value="true"/>
</bean>

以下示例显示如何使用jee配置对本地 EJB Stateless SessionBean 的引用以及许多属性:

<jee:local-slsb id="complexLocalEjb"
        jndi-name="ejb/RentalServiceBean"
        business-interface="com.foo.service.RentalService"
        cache-home="true"
        lookup-home-on-startup="true"
        resource-ref="true">
<jee:remote-slsb/>

<jee:remote-slsb/>元素配置对remote EJB Stateless SessionBean 的引用。

以下示例显示如何配置对不带jee的远程 EJB Stateless SessionBean 的引用:

<bean id="complexRemoteEjb"
        class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/MyRemoteBean"/>
    <property name="businessInterface" value="com.foo.service.RentalService"/>
    <property name="cacheHome" value="true"/>
    <property name="lookupHomeOnStartup" value="true"/>
    <property name="resourceRef" value="true"/>
    <property name="homeInterface" value="com.foo.service.RentalService"/>
    <property name="refreshHomeOnConnectFailure" value="true"/>
</bean>

以下示例显示如何使用jee配置对远程 EJB Stateless SessionBean 的引用:

<jee:remote-slsb id="complexRemoteEjb"
        jndi-name="ejb/MyRemoteBean"
        business-interface="com.foo.service.RentalService"
        cache-home="true"
        lookup-home-on-startup="true"
        resource-ref="true"
        home-interface="com.foo.service.RentalService"
        refresh-home-on-connect-failure="true">

9.1.2. jms 模式

jms元素用于配置与 JMS 相关的 bean,例如 Spring 的消息侦听器容器。这些元素在JMS chapter标题为JMS 命名空间支持的部分中进行了详细说明。有关此支持和jms元素本身的完整详细信息,请参见该章。

为了完整起见,要使用jms模式中的元素,您需要在 Spring XML 配置文件的顶部具有以下序言。以下代码段中的文本引用了正确的架构,以便您可以使用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"
    xmlns:jms="http://www.springframework.org/schema/jms" xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms.xsd">

    <!-- bean definitions here -->
</beans>

9.1.3. 使用\ <>

配置基于 Comments 的 MBean 导出中对此元素进行了详细说明。

9.1.4. 缓存架构

您可以使用cache元素启用对 Spring 的@CacheEvict@CachePut@CachingComments 的支持。它还支持基于声明式 XML 的缓存。有关详情,请参见启用缓存 Comments基于声明式 XML 的缓存

要使用cache模式中的元素,您需要在 Spring XML 配置文件的顶部具有以下序言。以下代码段中的文本引用了正确的架构,以便您可以使用cache名称空间中的元素:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache" xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

    <!-- bean definitions here -->
</beans>