# 整合

参考文档的这一部分涵盖了 Spring 框架与许多技术的集成。

# 1. REST 端点

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

  • [RestTemplate](#rest-resttemplate):原始的 Spring REST 客户机,具有同步的、模板的方法 API。

  • WebClient:一种非阻塞、反应性的替代方案,支持同步、异步以及流媒体场景。

从 5.0 开始,RestTemplate处于维护模式,只有少量的
更改请求和 bug 被接受。请考虑使用WebClient,它提供了一个更现代的 API,
支持同步、异步和流场景。

# 1.1.RestTemplate

RestTemplate在 HTTP 客户库上提供了更高级别的 API。它使得在单行中调用 REST 端点变得很容易。它公开了以下几组重载方法:

Method group 说明
getForObject 通过 get 检索表示。
getForEntity 使用 get 检索ResponseEntity(即状态、标题和正文)。
headFor标头 通过使用 head 检索资源的所有 header。
postForLocation 通过使用 POST 创建一个新资源,并从响应返回Location头。
postForObject 通过使用 POST 创建一个新资源,并从响应返回表示。
postForEntity 通过使用 POST 创建一个新资源,并从响应返回表示。
put 使用 PUT 创建或更新资源。
patchForObject 使用补丁更新资源并返回响应的表示。
注意,JDKHttpURLConnection不支持PATCH,但是 Apache
HttpComponents 和其他组件支持。
delete 使用 DELETE 删除指定 URI 上的资源。
optionsForAllow 通过使用 allow 检索资源的允许的 HTTP 方法。
exchange 在需要时提供额外的
灵活性的上述方法的更通用(且不那么固执己见)版本。它接受RequestEntity(包括 HTTP 方法、URL、headers、
和正文作为输入)并返回ResponseEntity

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

# 1.1.1.初始化

默认构造函数使用java.net.HttpURLConnection执行请求。你可以使用ClientHttpRequestFactory的实现切换到不同的 HTTP 库。以下是内置的支持:

  • Apache HttpComponents

  • 内蒂

  • OKHTTP

例如,要切换到 Apache HttpComponents,你可以使用以下方法:

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

每个ClientHttpRequestFactory都公开了特定于底层 HTTP 客户库的配置选项——例如,用于凭据、连接池和其他详细信息。

请注意,当
访问表示错误的响应的状态(例如 401)时,用于 HTTP 请求的java.net实现可能会引发异常。如果这是
问题,请切换到另一个 HTTP 客户库。
# 乌里斯

许多RestTemplate方法接受 URI 模板和 URI 模板变量,或者作为String变量参数,或者作为Map<String,String>

下面的示例使用String变量参数:

String result = restTemplate.getForObject(
        "https://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(
        "https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);

请记住,URI 模板是自动编码的,如下例所示:

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

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

可以使用RestTemplateuriTemplateHandler属性来定制 URI 的编码方式。或者,你可以准备一个java.net.URI并将其传递到一个RestTemplate方法,该方法接受URI

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

# Headers

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

String uriTemplate = "https://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();

你可以通过返回ResponseEntity的许多RestTemplate方法变量获得响应头。

# 1.1.2.身体

传入RestTemplate方法并从其返回的对象将在HttpMessageConverter的帮助下转换为 RAW 内容并从 RAW 内容转换。

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

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

你不需要显式地设置请求的内容类型标头。在大多数情况下,可以找到基于源Object类型的兼容消息转换器,并且所选择的消息转换器相应地设置内容类型。如果有必要,可以使用exchange方法显式地提供Content-Type请求头,这反过来会影响所选择的消息转换器。

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

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

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

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

# 1.1.3.消息转换

WebFlux

spring-web模块包含HttpMessageConverter契约,用于通过InputStreamOutputStream读取和写入 HTTP 请求和响应的主体。HttpMessageConverter实例用于客户端(例如,在RestTemplate中)和服务器端(例如,在 Spring MVC REST 控制器中)。

MIME 类型的具体实现是在框架中提供的,并且默认情况下,在客户端用RestTemplate注册,在服务器端用RequestMethodHandlerAdapter注册(参见配置消息转换器)。

HttpMessageConverter的实现方式在以下部分中进行了描述。对于所有转换器,都使用默认的媒体类型,但是你可以通过设置supportedMediaTypes Bean 属性来覆盖它。下表描述了每种实现方式:

MessageConverter 说明
StringHttpMessageConverter 一个HttpMessageConverter实现,它可以从 http
请求和响应中读取和写入String实例。默认情况下,此转换器支持所有文本媒体类型
text/*),并使用Content-Typetext/plain写。
FormHttpMessageConverter 一个HttpMessageConverter实现,它可以从 HTTP
请求和响应中读取和写入表单数据。默认情况下,此转换器读取和写入application/x-www-form-urlencoded媒体类型。表单数据被读取并写入MultiValueMap<String, String>。转换器还可以写入(但不能读取)多部分
MultiValueMap<String, Object>中读取的数据。默认情况下,multipart/form-data
支持的。在 Spring Framework5.2 中,对于
写入表单数据,可以支持额外的多部分子类型。有关更多详细信息,请咨询FormHttpMessageConverter的 Javadoc。
ByteArrayHttpMessageConverter 一个HttpMessageConverter实现,它可以从
HTTP 请求和响应中读写字节数组。默认情况下,此转换器支持所有媒体类型(*/*
,并使用Content-Typeapplication/octet-stream写。通过设置supportedMediaTypes属性并重写getContentType(byte[]),可以重写此
MarshallingHttpMessageConverter 一个HttpMessageConverter实现,它可以通过使用 Spring 的MarshallerUnmarshaller包中的抽象来读写 XML。
该转换器需要MarshallerUnmarshaller才能使用。你可以通过构造函数或 Bean 属性注入这些
。默认情况下,此转换器支持text/xmlapplication/xml
MappingJackson2HttpMessageConverter 一个HttpMessageConverter实现,它可以通过使用 Jackson 的ObjectMapper来读写 JSON。你可以根据需要使用 Jackson 的
提供的注释来定制 JSON 映射。当需要进一步控制时(对于需要为特定类型提供自定义 JSON
序列化器/反序列化器的情况),可以通过ObjectMapper属性注入自定义ObjectMapper。默认情况下,这个
转换器支持application/json
MappingJackson2XmlHttpMessageConverter 一个HttpMessageConverter实现,它可以通过使用JacksonXML (opens new window)扩展的XmlMapper来读写 XML。你可以根据需要通过使用 JAXB
或 Jackson 提供的注释来定制 XML 映射。当需要进一步控制时(对于需要为特定类型提供自定义 XML
序列化器/反序列化器的情况),可以通过XmlMapper属性注入自定义XmlMapper。默认情况下,这个
转换器支持application/xml
SourceHttpMessageConverter 一个HttpMessageConverter实现,它可以从 HTTP 请求和响应中读写javax.xml.transform.Source。只支持DOMSourceSAXSourceStreamSource。默认情况下,此转换器支持text/xmlapplication/xml
BufferedImageHttpMessageConverter 一个HttpMessageConverter实现,它可以从 HTTP 请求和响应中读写java.awt.image.BufferedImage。这个转换器读取
并写入 Java I/O API 支持的媒体类型。

# 1.1.4.JacksonJSON 视图

你可以指定JacksonJSON 视图 (opens new window)来序列化对象属性的一个子集,如下例所示:

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

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

ResponseEntity<String> response = template.exchange(requestEntity, String.class);
# 多部分

要发送多部分数据,你需要提供一个MultiValueMap<String, Object>,其值对于部分内容可以是Object,对于文件部分可以是Resource,对于带有标题的部分内容可以是HttpEntity。例如:

MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();

parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));

在大多数情况下,你不必为每个部分指定Content-Type。内容类型是基于所选择的HttpMessageConverter自动确定的,以序列化它,或者,在Resource的情况下,基于文件扩展名。如果有必要,你可以显式地为MediaType提供一个HttpEntity包装器。

一旦MultiValueMap准备好了,就可以将其传递给RestTemplate,如下所示:

MultiValueMap<String, Object> parts = ...;
template.postForObject("https://example.com/upload", parts, Void.class);

如果MultiValueMap包含至少一个非-String值,则Content-TypeFormHttpMessageConverter设置为multipart/form-data。如果MultiValueMap具有String值,则Content-Type默认为application/x-www-form-urlencoded。如果有必要,Content-Type也可以显式地设置。

# 1.2.使用AsyncRestTemplate(不推荐)

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

# 2. 远程和 Web 服务

Spring 通过各种技术为远程控制提供支持。远程支持简化了支持远程的服务的开发,这些服务是通过 Java 接口和对象作为输入和输出来实现的。目前, Spring 支持以下远程处理技术:

  • Java Web 服务: Spring 通过 JAX-WS 为 Web 服务提供远程支持。

  • AMQP:单独的 Spring AMQP 项目支持通过 AMQP 作为底层协议进行远程处理。

从 Spring Framework5.3 开始,出于安全原因和更广泛的行业支持,现在不赞成对几种远程技术的支持
。支持基础设施将从 Spring 框架中删除
,用于其下一个主要版本。

以下远程处理技术现已弃用,不会被替换:

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

  • Spring HTTP Invoker (Deprecated): Spring 提供了一种特殊的远程策略,该策略允许通过 HTTP 进行 Java 序列化,支持任何 Java 接口(就像 RMI 调用程序所做的那样)。对应的支持类是HttpInvokerProxyFactoryBeanHttpInvokerServiceExporter

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

  • JMS(已弃用):在spring-jms模块中的JmsInvokerServiceExporterJmsInvokerProxyFactoryBean类支持通过 JMS 作为底层协议进行远程处理。

在讨论 Spring 的远程功能时,我们使用了以下领域模型和相应的服务:

public class Account implements Serializable {

    private String name;

    public String getName(){
        return name;
    }

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

    public void insertAccount(Account account);

    public List<Account> getAccounts(String name);
}
// the implementation doing nothing at the moment
public class AccountServiceImpl implements AccountService {

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

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

本节首先通过使用 RMI 将服务公开给远程客户机,并稍微讨论一下使用 RMI 的缺点。然后继续以使用 Hessian 作为协议的示例进行说明。

# 2.1.AMQP

Spring AMQP 项目支持通过 AMQP 作为底层协议进行远程处理。欲了解更多详情,请访问 Spring AMQP 参考文献的Spring Remoting (opens new window)部分。

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

远程
接口未实现自动检测的主要原因是避免为远程调用者打开太多的门。目标对象可以
实现内部回调接口,例如InitializingBeanDisposableBean谁不想向调用者公开。

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

这是在配置便利和意外
暴露内部方法的风险之间的权衡。始终指定一个服务接口并不需要太多
的工作,并且对于特定方法的受控暴露,这会使你处于安全的一边。

# 2.2.选择技术时的考虑因素

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

当使用 RMI 时,你无法通过 HTTP 协议访问这些对象,除非你对 RMI 通信量进行了隧道处理。RMI 是一种非常重要的协议,因为它支持全对象序列化,当你使用需要在线序列化的复杂数据模型时,这一点非常重要。然而,RMI-JRMP 与 Java 客户机绑定在一起。这是一种从 Java 到 Java 的远程解决方案。

Spring 的 HTTP Invoker 是一个很好的选择,如果你需要基于 HTTP 的远程处理,但也需要依赖 Java 序列化。它与 RMI 调用者共享基本的基础设施,但使用 HTTP 作为传输。请注意,HTTP 调用程序不仅限于 Java-to-Java 远程操作,而且还限于客户端和服务器端的 Spring。(后者也适用于 Spring 的非 RMI 接口的 RMI 调用程序。)

在异构环境中操作时,Hessian 可能会提供重要的价值,因为它们明确地允许非 Java 客户机。然而,对非 Java 的支持仍然有限。已知的问题包括 Hibernate 对象的序列化与延迟初始化的集合的组合。如果你有这样的数据模型,可以考虑使用 RMI 或 HTTP 调用程序,而不是 Hessian。

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

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

# 2.3.Java Web 服务

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

  • 使用 JAX-WS 公开 Web 服务

  • 使用 JAX-WS 访问 Web 服务

除了 Spring Core 中对 JAX-WS 的支持之外, Spring 投资组合还具有Spring Web Services (opens new window),这是一种面向契约优先、文档驱动的 Web 服务的解决方案——强烈推荐用于构建现代的、面向未来的 Web 服务。

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

Spring 为 JAX-WS Servlet 端点实现提供了一个方便的基类:。为了公开我们的AccountService,我们扩展 Spring 的SpringBeanAutowiringSupport类,并在此实现我们的业务逻辑,通常将调用委派给业务层。我们使用 Spring 的@Autowired注释来表示对 Spring 管理的 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 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 环境中,默认情况下就是这样,它使用 JAX-WS Servlet 端点部署的标准契约。有关详细信息,请参见各种 Java EE Web 服务教程。

# 2.3.2.使用 JAX-WS 导出独立的 Web 服务

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

在此场景中,端点实例被定义为 Spring bean 本身并作为其进行管理。它们在 JAX-WS 引擎中注册,但它们的生命周期取决于 Spring 应用程序上下文。这意味着你可以将 Spring 功能(例如显式依赖注入)应用到端点实例。通过@Autowired进行注释驱动的注入也同样有效。下面的示例展示了如何定义这些 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派生,因为本例中的端点是完全由 Spring 管理的 Bean。这意味着端点实现可以如下(不声明任何超类——并且 Spring 的@Autowired配置注释仍然受到尊重):

@WebService(serviceName="AccountService")
public class AccountServiceEndpoint {

    @Autowired
    private AccountService biz;

    @WebMethod
    public void insertAccount(Account acc) {
        biz.insertAccount(acc);
    }

    @WebMethod
    public List<Account> getAccounts(String name) {
        return biz.getAccounts(name);
    }
}

# 2.3.3.通过使用 JAX-WS RI 的 Spring 支持来导出 Web 服务

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

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

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

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

# 2.3.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="https://example/"/>
    <property name="serviceName" value="AccountService"/>
    <property name="portName" value="AccountServiceEndpointPort"/>
</bean>
1 其中serviceInterface是客户机使用的业务接口。

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>

从客户机代码中,我们可以像访问普通类一样访问 Web 服务,如下例所示:

public class AccountClientImpl {

    private AccountService service;

    public void setService(AccountService service) {
        this.service = service;
    }

    public void foo() {
        service.insertAccount(...);
    }
}
上面稍微简化了一下,因为 JAX-WS 要求端点接口
和实现类使用@WebService@SOAPBinding等注释
注释。这意味着你不能(轻松地)使用普通的 Java 接口和
实现类作为 JAX-WS 端点工件;你需要首先对它们
进行相应的注释。查看 JAX-WS 文档,了解有关这些需求的详细信息。

# 2.4.RMI(已弃用)

截至 Spring 框架 5.3,RMI 支持是不受欢迎的,并且不会被替换。

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

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

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

# 2.4.2.在客户端的服务中链接

我们的客户机是一个简单的对象,它使用AccountService来管理帐户,如下例所示:

public class SimpleObject {

    private AccountService accountService;

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

    // additional methods using the accountService
}

为了在客户端上链接服务,我们创建了一个单独的 Spring 容器,以包含以下简单的对象和服务连接配置位:

<bean class="example.SimpleObject">
    <property name="accountService" ref="accountService"/>
</bean>

<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
    <property name="serviceUrl" value="rmi://HOST:1199/AccountService"/>
    <property name="serviceInterface" value="example.AccountService"/>
</bean>

这就是我们在客户端上支持远程帐户服务所需要做的全部工作。 Spring 透明地创建调用程序并通过RmiServiceExporter远程启用该帐户服务。在客户端,我们使用RmiProxyFactoryBean将其链接进来。

# 2.5.使用 Hessian 通过 HTTP 远程调用服务(已弃用)

截至 Spring 框架 5.3,Hessian 支持已被弃用,并且不会被替换。

Hessian 提供了一种基于二进制 HTTP 的远程处理协议。它是由 Caucho 开发的,你可以在https://www.caucho.com/ (opens new window)上找到有关黑森本身的更多信息。

# 2.5.1.黑森

Hessian 通过 HTTP 进行通信,并通过使用自定义 Servlet 进行通信。通过使用 Spring 的DispatcherServlet原则(参见webmvc.html),我们可以连接这样的 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原则,那么你可能知道,现在你必须在remoting-servlet.xml目录中创建一个名为remoting-servlet.xml的 Spring 容器配置资源(以你的 Servlet 的名字命名)。应用程序上下文将在下一节中使用。

或者,考虑使用 Spring 的更简单的HttpRequestHandlerServlet。这样做可以让你在根应用程序上下文(默认情况下,在WEB-INF/applicationContext.xml中)中嵌入远程导出定义,并使用单独的 Servlet 定义指向特定的导出 bean。在这种情况下,每个 Servlet 名称需要匹配其目标输出器的 Bean 名称。

# 2.5.2.使用HessianServiceExporter暴露 bean

在新创建的名为remoting-servlet.xml的应用程序上下文中,我们创建了一个HessianServiceExporter来导出我们的服务,如下例所示:

<bean id="accountService" class="example.AccountServiceImpl">
    <!-- any additional properties, maybe a DAO? -->
</bean>

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

现在,我们已经准备好在客户端的服务链接。没有指定显式的处理程序映射(以将请求 URL 映射到服务上),因此我们使用BeanNameUrlHandlerMapping。因此,该服务在包含DispatcherServlet实例映射(如前面定义的)中的 Bean 名称所指示的 URL 处导出:[https://HOST:8080/remoting/AccountService](https://HOST:8080/remoting/AccountService)

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

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

在后一种情况下,你应该在web.xml中为这个输出器定义一个相应的 Servlet,其最终结果是相同的:输出器被映射到位于/remoting/AccountService的请求路径。请注意, Servlet 名称需要与目标输出器的 Bean 名称匹配。下面的示例展示了如何做到这一点:

<servlet>
    <servlet-name>accountExporter</servlet-name>
    <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>accountExporter</servlet-name>
    <url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>

# 2.5.3.在客户端的服务中链接

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

<bean class="example.SimpleObject">
    <property name="accountService" ref="accountService"/>
</bean>

<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
    <property name="serviceUrl" value="https://remotehost:8080/remoting/AccountService"/>
    <property name="serviceInterface" value="example.AccountService"/>
</bean>

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

Hessian 的优点之一是,我们可以轻松地应用 HTTP 基本身份验证,因为这两个协议都是基于 HTTP 的。例如,可以通过使用web.xml安全特性来应用正常的 HTTP 服务器安全机制。通常,在此不需要使用每个用户的安全凭据。相反,你可以使用在HessianProxyFactoryBean级别定义的共享凭据(类似于 JDBCDataSource),如下例所示:

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
    <property name="interceptors" ref="authorizationInterceptor"/>
</bean>

<bean id="authorizationInterceptor"
        class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
    <property name="authorizedRoles" value="administrator,operator"/>
</bean>

在前面的示例中,我们显式地提到了BeanNameUrlHandlerMapping并设置了一个拦截器,以便只让管理员和操作员调用在此应用程序上下文中提到的 bean。

前面的示例并未展示一种灵活的安全基础架构。对于
关于安全性的更多选项,请查看 Spring 安全性项目
athttps://projects.spring.io/spring-security/ (opens new window)

# 2.6. Spring HTTP Invoker(不推荐)

在 Spring Framework5.3 中,HTTP Invoker 支持已被弃用,不会被替换。

与 Hessian 相反, Spring HTTP 调用程序都是轻量级协议,它们使用自己的 Slim 序列化机制,并使用标准的 Java 序列化机制通过 HTTP 公开服务。如果你的参数和返回类型是复杂的类型,无法通过使用 Hessian 使用的序列化机制进行序列化,那么这将具有巨大的优势(在选择远程技术时,请参阅下一节以了解更多的考虑因素)。

在这种情况下, Spring 使用 JDK 或 ApacheHttpComponents提供的标准工具来执行 HTTP 调用。如果你需要更高级、更易用的功能,请使用后者。有关更多信息,请参见hc.apache.org/httpcomponents-client-ga/ (opens new window)

注意由不安全的 Java 反序列化引起的漏洞:
在反序列化步骤期间,被操纵的输入流可能导致服务器上执行不需要的代码
。因此,不要将 HTTP Invoker
端点公开给不受信任的客户端。相反,只在你自己的服务之间公开它们。
总的来说,我们强烈建议使用任何其他消息格式(例如 JSON),

如果你担心 Java 序列化带来的安全漏洞,
请考虑核心 JVM 级别的通用序列化过滤机制,
最初是为 JDK9 开发的,但后来移植到了 JDK8,同时是 7 号和 6 号。见https://blogs.oracle.com/java-platform-group/entry/incoming_filter_serialization_data_a (opens new window)https://openjdk.java.net/jeps/290 (opens new window)

# 2.6.1.公开服务对象

为服务对象设置 HTTP 调用程序基础结构与使用 Hessian 进行相同设置的方式非常相似。由于 Hessian 支持提供了HessianServiceExporter, Spring 的 Httpinvoker 支持提供了org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter

要在 Spring Web MVC中公开(前面提到过),需要在 Dispatcher 的应用程序上下文中设置以下配置,如下例所示:

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

这样的导出定义通过DispatcherServlet实例的标准映射工具公开,如关于黑森的章节中所解释的那样。

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

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

此外,可以在web.xml中为该输出器定义相应的 Servlet,其 Servlet 名称与目标输出器的 Bean 名称匹配,如下例所示:

<servlet>
    <servlet-name>accountExporter</servlet-name>
    <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>accountExporter</servlet-name>
    <url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>

# 2.6.2.在客户端的服务中链接

同样,从客户机在服务中进行链接,与使用 Hessian 时的方式非常相似。通过使用代理, Spring 可以将对 HTTP POST 请求的调用转换为指向导出服务的 URL。下面的示例展示了如何配置这种安排:

<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
    <property name="serviceUrl" value="https://remotehost:8080/remoting/AccountService"/>
    <property name="serviceInterface" value="example.AccountService"/>
</bean>

如前所述,你可以选择要使用的 HTTP 客户机。默认情况下,HttpInvokerProxy使用 JDK 的 HTTP 功能,但也可以通过设置httpInvokerRequestExecutor属性来使用 ApacheHttpComponents客户端。下面的示例展示了如何做到这一点:

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

# 2.7.JMS(已弃用)

在 Spring Framework5.3 中,JMS 远程支持是不受欢迎的,并且不会被替换。

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

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

package com.foo;

public interface CheckingAccountService {

    public void cancelAccount(Long accountId);
}

在服务器端使用了前面接口的以下简单实现:

package com.foo;

public class SimpleCheckingAccountService implements CheckingAccountService {

    public void cancelAccount(Long accountId) {
        System.out.println("Cancelling account [" + accountId + "]");
    }
}

以下配置文件包含在客户机和服务器上共享的 JMS-Infrastructure 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
        https://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>

# 2.7.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
        https://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("com/foo/server.xml", "com/foo/jms.xml");
    }
}

# 2.7.2.客户端配置

客户机只需要创建一个实现约定接口(CheckingAccountService)的客户端代理。

下面的示例定义了可以注入到其他客户端对象中的 bean(代理负责通过 JMS 将调用转发到服务器端对象):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://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("com/foo/client.xml", "com/foo/jms.xml");
        CheckingAccountService service = (CheckingAccountService) ctx.getBean("checkingAccountService");
        service.cancelAccount(new Long(10));
    }
}

# 3. EnterpriseJavaBeans 集成

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

然而,需要注意的是,使用 Spring 并不妨碍你使用 EJB。实际上, Spring 使得访问 EJB 和在其中实现 EJB 和功能变得更加容易。此外,使用 Spring 来访问由 EJB 提供的服务允许这些服务的实现稍后在本地 EJB、远程 EJB 或 POJO(纯旧的 Java 对象)变体之间透明地进行切换,而无需更改客户端代码。

在本章中,我们将研究 Spring 如何帮助你访问和实现 EJB。 Spring 在访问无状态会话 bean 时提供了特定的值,因此我们首先讨论这个主题。

# 3.1.访问 EJB

本节介绍如何访问 EJB。

# 3.1.1.概念

Bean 要在本地或远程无状态会话上调用方法,客户端代码通常必须执行 JNDI 查找以获得(本地或远程)EJB 主对象,然后在该对象上使用create方法调用以获得实际的(本地或远程)EJB 对象。然后在 EJB 上调用一个或多个方法。

为了避免重复的低级代码,许多 EJB 应用程序使用服务定位器和业务委托模式。这些比在整个客户机代码中进行大量的 JNDI 查找更好,但是它们通常的实现有很大的缺点:

  • 通常,使用 EJB 的代码依赖于服务定位器或业务委托单例,这使得很难进行测试。

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

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

Spring 方法是允许创建和使用代理对象(通常在 Spring 容器内配置),这些代理对象充当无码业务委托。你不需要在手工编码的业务委托中编写另一个服务定位器、另一个 JNDI 查找或重复的方法,除非你实际上在这样的代码中添加了真正的价值。

# 3.1.2.访问本地 SLSB

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

public interface MyComponent {
    ...
}

Bean 使用业务方法接口模式的主要原因之一是确保本地接口中的方法签名和实现类之间的同步是自动的。另一个原因是,如果切换到服务的 POJO(纯旧的 Java 对象)实现是有意义的,那么以后切换到 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>

由于 Spring AOP 框架,大量的工作在幕后进行,尽管你并不是被迫使用 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 层代码(或其他 EJB 客户机代码)不依赖于 EJB 的使用。要用 POJO 或模拟对象或其他测试存根替换此 EJB 引用,我们可以在不更改一行 Java 代码的情况下更改myComponent Bean 定义。此外,我们不需要编写一行 JNDI 查找或其他 EJB 管道代码作为应用程序的一部分。

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

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

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

# 3.1.3.访问远程 SLSB

访问远程 EJB 本质上与访问本地 EJB 相同,只是使用了SimpleRemoteStatelessSessionProxyFactoryBean<jee:remote-slsb>配置元素。当然,不管有没有 Spring,远程调用语义都适用:在另一台计算机中,对另一 VM 中的对象上的方法的调用有时确实需要在使用场景和故障处理方面进行不同的处理。

Spring 的 EJB 客户机支持比非 Spring 方法增加了一个优势。通常情况下,EJB 客户机代码在本地或远程调用 EJB 之间容易地来回切换是有问题的。这是因为远程接口方法必须声明它们抛出RemoteException,而客户端代码必须处理这个问题,而本地接口方法则不需要。为本地 EJB 编写的客户端代码需要迁移到远程 EJB,通常需要对其进行修改,以添加对远程异常的处理,而为远程 EJB 编写的、需要转移到本地 EJB 的客户机代码可以保持不变,但可以对远程异常进行大量不必要的处理,或者进行修改以删除该代码。使用 Spring 远程 EJB 代理,你可以不声明在你的业务方法接口和实现 EJB 代码中抛出的任何RemoteException,而是具有相同的远程接口(除了它确实抛出RemoteException),并依赖代理来动态地对待这两个接口,就像它们是相同的一样。也就是说,客户机代码不必处理选中的RemoteException类。在 EJB 调用期间抛出的任何实际RemoteException都将被重新抛出为未选中的RemoteAccessException类,它是RuntimeException的子类。然后,你可以在本地 EJB 或远程 EJB(甚至是普通的 Java 对象)实现之间随意切换目标服务,而不需要客户机代码知道或关心。当然,这是可选的:没有什么可以阻止你在业务接口中声明RemoteException

# 3.1.4.访问 EJB2.x slsbs 与 EJB3slsbs

通过 Spring 访问 EJB2.x 会话 bean 和 EJB3 会话 bean 在很大程度上是透明的。 Spring 的 EJB 访问器,包括<jee:local-slsb><jee:remote-slsb>工具,在运行时透明地适应实际组件。如果找到了 home 接口(EJB2.x 样式),他们将处理该接口;如果没有 home 接口,他们将执行直接的组件调用(EJB3 样式)。

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

# 4. JMS(Java 消息服务)

Spring 提供了一种 JMS 集成框架,该框架简化了 JMS API 的使用,其方式与 Spring 的集成为 JDBC API 提供的方式大致相同。

JMS 可以大致分为两个功能领域,即消息的产生和使用。JmsTemplate类用于消息产生和同步消息接收。对于类似于 Java EE 的消息驱动 Bean 风格的异步接收, Spring 提供了许多消息侦听器容器,你可以使用这些容器来创建消息驱动的 POJO。 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包提供了用于管理 JMS 目的地的各种策略,例如为存储在 JNDI 中的目的地提供服务定位器。

org.springframework.jms.annotation包提供了必要的基础设施,通过使用@JmsListener支持注释驱动的侦听器端点。

org.springframework.jms.config包提供了jms名称空间的解析器实现,以及用于配置侦听器容器和创建侦听器端点的 Java Config 支持。

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

在 Spring Framework5 中, Spring 的 JMS 包完全支持 JMS2.0,并且要求
JMS2.0API 在运行时存在。我们建议使用与 JMS2.0 兼容的提供者。

如果你的系统中使用了较旧的消息代理,则可以尝试为你现有的代理生成升级到与
JMS2.0 兼容的驱动程序。或者,你也可以
尝试在基于 JMS1.1 的驱动程序上运行,只需将 JMS2.0API jar 放在
Classpath 上,但仅对你的驱动程序使用与 JMS1.1 兼容的 API。 Spring 的 JMS 支持
默认情况下遵循 JMS1.1 约定,因此通过相应的配置,它确实
支持这样的场景。但是,请仅在转换场景中考虑这一点。

# 4.1.使用 Spring JMS

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

# 4.1.1.使用JmsTemplate

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

使用JmsTemplate的代码只需要实现回调接口,从而为它们提供一个明确定义的高级契约。当给定由JmsTemplate中的调用代码提供的Session时,MessageCreator回调接口将创建一条消息。为了允许更复杂地使用 JMS API,SessionCallback提供了 JMS 会话,而ProducerCallback公开了一个SessionMessageProducer对。

JMS API 公开了两种类型的发送方法,一种是将交付模式、优先级和实时作为服务质量参数,另一种是不接受 QoS 参数并使用默认值。由于JmsTemplate具有许多发送方法,所以将 QoS 参数设置为 Bean 属性已公开,以避免发送方法数量的重复。类似地,同步接收调用的超时值是通过使用setReceiveTimeout属性设置的。

一些 JMS 提供程序允许通过ConnectionFactory的配置在管理上设置默认的 QoS 值。这样做的结果是,调用MessageProducer实例的send方法(send(Destination destination, Message message))所使用的 QoS 默认值与 JMS 规范中指定的值不同。因此,为了提供一致的 QoS 值管理,必须通过将布尔属性isExplicitQosEnabled设置为true,明确地使JmsTemplate能够使用其自己的 QoS 值。

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

一旦配置好,JmsTemplate类的实例是线程安全的。这是
重要的,因为这意味着你可以配置JmsTemplate的单个实例,然后安全地将此共享引用注入多个协作者。要使
清晰,JmsTemplate是有状态的,因为它维护对ConnectionFactory的引用,但该状态不是会话状态。

在 Spring Framework4.1 中,JmsMessagingTemplate构建在JmsTemplate之上,并提供了与消息抽象的集成——即org.springframework.messaging.Message。这使你能够以通用的方式创建要发送的消息。

# 4.1.2.连接

JmsTemplate需要引用ConnectionFactoryConnectionFactory是 JMS 规范的一部分,是使用 JMS 的入口点。客户机应用程序将其用作工厂,以创建与 JMS 提供者的连接,并封装各种配置参数,其中许多是特定于供应商的,例如 SSL 配置选项。

当在 EJB 中使用 JMS 时,供应商提供 JMS 接口的实现,以便他们可以参与声明式事务管理并执行连接和会话的池。为了使用此实现,Java EE 容器通常要求你在 EJB 或 Servlet 部署描述符中将 JMS 连接工厂声明为resource-ref。为了确保在 EJB 中使用JmsTemplate这些特性,客户端应用程序应该确保它引用ConnectionFactory的托管实现。

# 缓存消息传递资源

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

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

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

# 使用SingleConnectionFactory

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

# 使用CachingConnectionFactory

CachingConnectionFactory扩展了SingleConnectionFactory的功能,并添加了SessionMessageProducerMessageConsumer实例的缓存。初始缓存大小设置为1。你可以使用sessionCacheSize属性来增加缓存的会话的数量。请注意,实际缓存的会话的数量要多于这个数量,因为会话是基于它们的确认模式进行缓存的,因此当sessionCacheSize设置为 1 时,最多可以有 4 个缓存的会话实例(每个确认模式一个)。MessageProducerMessageConsumer实例在其所属会话中进行缓存,并且在缓存时还考虑到生产者和消费者的独特属性。消息生成器是根据它们的目的地进行缓存的。MessageConsumer 是基于由 Destination、Selector、noLocal Delivery 标志和持久订阅名称(如果创建持久消费者的话)组成的键进行缓存的。

# 4.1.3.目的地管理

Destinations 作为ConnectionFactory实例,是可以在 JNDI 中存储和检索的 JMS 管理的对象。在配置 Spring 应用程序上下文时,可以使用 JNDI工厂类或对对象对 JMS 目的地的引用执行依赖注入。但是,如果应用程序中有大量的目的地,或者如果有 JMS 提供程序独有的高级目的地管理功能,那么这种策略通常会很麻烦。这种高级目的地管理的示例包括创建动态目的地或支持目的地的分级命名空间。JmsTemplate将目标名称的解析委托给实现DestinationResolver接口的 JMS 目标对象。DynamicDestinationResolverJmsTemplate使用的默认实现,并适应解析动态目标。还提供了一个JndiDestinationResolver来充当 JNDI 中包含的目标的服务定位器,并可选地返回到DynamicDestinationResolver中包含的行为。

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

布尔属性pubSubDomain用于配置JmsTemplate,同时了解正在使用的 JMS 域。默认情况下,此属性的值为 false,表示要使用点对点域Queues。此属性(由JmsTemplate使用)通过DestinationResolver接口的实现来确定动态目标分辨率的行为。

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

# 4.1.4.消息监听器容器

在 EJB 世界中,JMS 消息最常见的用途之一是驱动消息驱动 Bean。 Spring 提供了一种以不将用户绑定到 EJB 容器的方式创建消息驱动 POJO 的解决方案。(关于 Spring 的 MDP 支持的详细介绍,请参见异步接收:消息驱动的 POJO。)自 Spring Framework4.1 以来,端点方法可以使用@JmsListener进行注释——有关更多详细信息,请参见注释驱动的监听器端点

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

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

  • [SimpleMessageListenerContainer]

  • [DefaultMessageListenerContainer](#jms-mdp-default)

# 使用SimpleMessageListenerContainer

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

虽然SimpleMessageListenerContainer不允许参与外部
托管事务,但它确实支持本机 JMS 事务。要启用此功能,
你可以将sessionTransacted标志切换到true,或者在 XML 命名空间中,将acknowledge属性设置为transacted。从侦听器抛出的异常将导致
回滚,并重新传递消息。或者,考虑使用CLIENT_ACKNOWLEDGE模式,这在异常的情况下也提供了重新交付,但是
不使用已处理的Session实例,因此,不包括事务协议中的任何其他Session操作(例如发送响应消息)。
默认的AUTO_ACKNOWLEDGE模式不能提供适当的可靠性保证,当监听器执行失败时,
消息可能会丢失(因为提供者在监听器调用后自动
确认每条消息,没有异常情况要传播到
提供程序)或当侦听器容器关闭时(你可以通过设置
acceptMessagesWhileStopping标志来配置此项)。确保在
可靠性需要的情况下使用事务会话(例如,用于可靠的队列处理和持久的主题订阅)。
# 使用DefaultMessageListenerContainer

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

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

当代理发生故障时,这个容器还具有可恢复的功能。默认情况下,一个简单的BackOff实现每五秒重试一次。你可以为更细粒度的恢复选项指定一个自定义BackOff实现。参见[ExponentialBackOff](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/util/backoff/exponentialbackoff.html)中的一个例子。

像它的兄弟([SimpleMessageListenerContainer](#JMS-MDP-Simple))一样,DefaultMessageListenerContainer支持本机 JMS 事务,并允许
自定义确认模式。如果对于你的场景是可行的,那么对于外部管理的事务,强烈建议使用
—也就是说,如果你可以在 JVM 失效的情况下使用
偶尔重复的消息。业务逻辑中的自定义重复消息
检测步骤可以覆盖此类情况—例如,
形式的业务实体存在检查或协议表检查。
任何这样的安排都比另一种选择明显更有效:
用 XA 事务(通过配置DefaultMessageListenerContainerJtaTransactionManager)来覆盖
接收的 JMS 消息。作为你的
消息侦听器中的业务逻辑的执行(包括数据库操作等)。
默认的AUTO_ACKNOWLEDGE模式不能提供适当的可靠性保证,当监听器执行失败时,
消息可能会丢失(因为在监听器调用之后,提供程序会自动
确认每条消息,没有异常情况要传播到
提供程序)或当侦听器容器关闭时(你可以通过设置
acceptMessagesWhileStopping标志来配置此项)。确保在
可靠性需要的情况下使用事务会话(例如,用于可靠的队列处理和持久的主题订阅)。

# 4.1.5.事务管理

Spring 提供了一个JmsTransactionManager,它为单个 JMSConnectionFactory管理事务。这使得 JMS 应用程序能够利用 Spring 的托管事务特性,如数据访问章节的事务管理部分中所述。JmsTransactionManager执行本地资源事务,将指定的ConnectionFactory的 JMS 连接/会话对绑定到线程。JmsTemplate自动检测此类事务资源并对其进行相应操作。

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

你还可以使用JmsTemplateJtaTransactionManager和一个具有 XA 功能的 JMSConnectionFactory来执行分布式事务。请注意,这需要使用 JTA 事务管理器以及正确配置 XA 的 ConnectionFactory。(检查你的 Java EE 服务器或 JMS 提供者的文档。

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

# 4.2.发送消息

JmsTemplate包含许多发送消息的方便方法。Send 方法通过使用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便利基类派生,它为 JMS 配置提供了预先构建的 Bean 属性。

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

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

# 4.2.1.使用消息转换器

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

沙盒目前包括MapMessageConverter,它使用反射在 JavaBean 和MapMessage之间进行转换。你可能自己实现的其他流行的实现选择是转换器,它们使用现有的 XML 编组包(例如 JAXB 或 XStream)来创建表示对象的TextMessage

为了适应消息的属性、标头和主体的设置,而这些属性、标头和主体通常不能封装在转换器类中,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}
    }
}

# 4.2.2.使用SessionCallbackProducerCallback

虽然发送操作涵盖了许多常见的使用场景,但有时你可能希望在 JMSSessionMessageProducer上执行多个操作。SessionCallbackProducerCallback分别暴露 JMSSessionSession/MessageProducer对。JmsTemplate上的execute()方法运行这些回调方法。

# 4.3.接收消息

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

# 4.3.1.同步接收

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

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

Spring 还通过使用@JmsListener注释来支持带注释的侦听器端点,并提供了一种以编程方式注册端点的开放基础设施。
这是迄今为止设置异步接收器的最方便的方式。
有关更多详细信息,请参见启用监听器端点注释

Bean(MDB)在 EJB 世界中,以类似于消息驱动的方式,消息驱动的 POJO 充当 JMS 消息的接收者。MDP 上的一个限制(但请参见[使用MessageListenerAdapter](#JMS-Receiving-Async-Message-Listener-Adapter))是它必须实现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 (opens new window))的 Spring Javadoc。

# 4.3.3.使用SessionAwareMessageListener接口

SessionAwareMessageListener接口是一个 Spring 特定的接口,它提供了与 JMSMessageListener接口类似的契约,但也使消息处理方法能够访问从其接收Message的 JMSSession。下面的清单显示了SessionAwareMessageListener接口的定义:

package org.springframework.jms.listener;

public interface SessionAwareMessageListener {

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

如果你希望你的 MDP 能够响应任何接收到的消息(通过使用onMessage(Message, Session)方法中提供的Session方法),你可以选择让你的 MDP 实现这个接口(优先于标准的 JMSMessageListener接口)。 Spring 附带的所有消息侦听器容器实现都支持实现MessageListenerSessionAwareMessageListener接口的 MDP。实现SessionAwareMessageListener的类需要注意的是,它们随后会通过接口绑定到 Spring。是否使用它的选择完全取决于作为应用程序开发人员或架构师的你。

注意,onMessage(..)接口的SessionAwareMessageListener方法抛出JMSException。与标准的 JMSMessageListener接口相反,当使用SessionAwareMessageListener接口时,客户端代码负责处理任何抛出的异常。

# 4.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>

下一个示例显示了另一个只能处理接收 JMSTextMessage消息的 MDP。请注意消息处理方法实际上是如何被称为receive的(MessageListenerAdapter中的消息处理方法的名称默认为handleMessage),但它是可配置的(如你在本节稍后部分中看到的)。还请注意receive(..)方法是如何强类型的,以便仅接收和响应 JMSTextMessage消息。下面的清单显示了TextMessageDelegate接口的定义:

public interface TextMessageDelegate {

    void receive(TextMessage message);
}

下面的清单显示了一个实现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的类型的 JMSMessage,则抛出一个IllegalStateException(并随后吞下)。MessageListenerAdapter类的另一个功能是,如果处理程序方法返回一个非 void 值,则能够自动发送回响应Message。考虑以下接口和类:

public interface ResponsiveTextMessageDelegate {

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

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

# 4.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>

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

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

Bean 以下定义创建了事务管理器:

<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 我们的交易经理。

# 4.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实例定义一个共享线程池,如果你碰巧使用了多个适配器。

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

见 Javadoc 的[JmsMessageEndpointManager](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jms/listener/endpoint/jmssageendpointmanager.html),[JmsActivationSpecConfig](https://DOCS. Spring.io/.io/ Spring.io/ Spring-framework/DOCS/5.3.16/javapi/javadoc/org/msframework/msframework/jmsframework/jms

Spring 还提供了不绑定到 JMS 的通用 JCA 消息端点管理器:org.springframework.jca.endpoint.GenericMessageEndpointManager。该组件允许使用任何消息侦听器类型(例如 JMSMessageListener)和任何特定于提供者的ActivationSpec对象。查看你的 JCA 提供者的文档以了解连接器的实际功能,并查看[GenericMessageEndpointManager](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jca/endpoint/genericmessageendpointmanager.html)Javadoc 以获得 Spring 特定的配置细节。

基于 JCA 的消息端点管理非常类似于 EJB2.1 消息驱动的 bean。
它使用相同的底层资源提供者契约。与 EJB2.1MDB 一样,你也可以在 Spring 上下文中使用你的 JCA 提供程序支持的任何
消息侦听器接口。尽管如此,
Spring 仍然为 JMS 提供了明确的“便利”支持,因为 JMS 是 JCA 端点管理合同中使用的最常见的端点 API

# 4.5.注释驱动的监听器端点

异步接收消息的最简单方法是使用带注释的侦听器端点基础结构。简而言之,它允许你将托管 Bean 方法公开为 JMS 侦听器端点。下面的示例展示了如何使用它:

@Component
public class MyService {

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

前面示例的思想是,每当javax.jms.Destination``myDestination上有消息可用时,都会相应地调用processOrder方法(在这种情况下,使用 JMS 消息的内容,类似于[MessageListenerAdapter](#JMS-receiving-async-message-listener-adapter)提供的内容)。

通过使用JmsListenerContainerFactory,带注释的端点基础设施在幕后为每个带注释的方法创建了一个消息侦听器容器。这样的容器不是针对应用程序上下文注册的,而是可以通过使用JmsListenerEndpointRegistry Bean 为管理目的而容易地定位。

@JmsListener是 Java8 上的一个可重复注释,因此你可以通过向它添加额外的@JmsListener声明,将
多个 JMS 目标与相同的方法关联起来。

# 4.5.1.启用监听器端点注释

要启用对@JmsListener注释的支持,你可以将@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方法,其核心轮询大小为 3 个线程,最大池大小为 10 个线程。

你可以定制用于每个注释的侦听器容器工厂,也可以通过实现JmsListenerConfigurer接口来配置显式默认值。只有当至少有一个端点在没有特定容器工厂的情况下注册时,才需要默认设置。参见实现[JmsListenerConfigurer]的类的 Javadoc(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jms/annotation/jmslistenerconfigurer.html)以获取详细信息和示例。

如果你更喜欢XML 配置,那么可以使用<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>

# 4.5.2.程序化端点注册

JmsListenerEndpoint提供了一个 JMS 端点模型,并负责为该模型配置容器。除了通过JmsListener注释检测到的端点之外,该基础结构还允许你以编程方式配置端点。下面的示例展示了如何做到这一点:

@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以编程方式只注册你的端点。

# 4.5.3.带注释的端点方法签名

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

@Component
public class MyService {

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

可以在 JMS Listener 端点中注入的主要元素如下:

  • RAWjavax.jms.Message或其任一子类(前提是它匹配传入的消息类型)。

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

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

  • @Header-带注释的方法参数,以提取特定的标头值,包括标准的 JMS 标头。

  • 一个@Headers-带注释的参数,该参数也必须分配给java.util.Map,以获得对所有标题的访问权限。

  • 不是受支持类型之一的非注释元素(MessageSession)被认为是有效负载。你可以通过使用@Payload对参数进行注释来使其显式。你还可以通过添加一个额外的@Valid来打开验证。

注入 Spring 的Message抽象的能力对于受益于存储在特定传输消息中的所有信息而不依赖特定传输 API 特别有用。下面的示例展示了如何做到这一点:

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

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

例如,如果我们希望在处理它之前确保我们的Order是有效的,那么我们可以用@Valid对有效负载进行注释,并配置必要的验证器,如下例所示:

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

# 4.5.4.反应管理

[MessageListenerAdapter](#JMS-Receiving-Async-Message-Listener-Adapter)中现有的支持已经允许你的方法具有非void返回类型。在这种情况下,调用的结果被封装在javax.jms.Message中,在原始消息的JMSReplyTo头中指定的目标中或在侦听器上配置的默认目标中发送。现在,你可以通过使用消息抽象的@SendTo注释来设置默认的目标。

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

@JmsListener(destination = "myDestination")
@SendTo("status")
public OrderStatus processOrder(Order order) {
    // order processing
    return status;
}
如果你有几个@JmsListener-注释的方法,你还可以在类级别上放置@SendTo注释,以共享默认的回复目标。

如果需要以与传输无关的方式设置额外的头,则可以使用类似于以下方法的方法返回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;
    }
}

# 4.6.JMS 名称空间支持

Spring 提供了用于简化 JMS 配置的 XML 命名空间。要使用 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 https://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms.xsd">

    <!-- bean definitions here -->

</beans>
1 引用 JMS 模式。

名称空间由三个顶级元素组成:<annotation-driven/><listener-container/><jca-listener-container/><annotation-driven/>启用注释驱动的监听器端点<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 定义,如[使用MessageListenerAdapter](#JMS-receiving-async-message-listener-adapter)所示。除了前面示例中显示的属性外,listener元素还可以包含几个可选的属性。下表描述了所有可用的属性:

Attribute 说明
id Bean 主机侦听器容器的名称。如果没有指定,将自动生成一个 Bean 名称
destination (required) 此侦听器的目标名称,通过DestinationResolver策略解析。
ref (required) Bean 处理程序对象的名称。
method 要调用的处理程序方法的名称。如果ref属性指向MessageListener或 Spring SessionAwareMessageListener,则可以省略该属性。
response-destination 要向其发送响应消息的默认响应目的地的名称。这是
应用于不带有JMSReplyTo字段的请求消息的情况。此目的地的
类型由侦听器容器的response-destination-type属性确定。请注意,这仅适用于具有
返回值的侦听器方法,为此,每个结果对象都转换为响应消息。
subscription 持久订阅的名称(如果有的话)。
selector 此侦听器的可选消息选择器。
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](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jms/listener/abstractmessagelistenercontainer.html)的类级 Javadoc 及其具体的子类,以获得有关单个属性的更多详细信息。Javadoc 还提供了对事务选择和消息重新交付场景的讨论。

Attribute 说明
container-type 此侦听器容器的类型。可用的选项是defaultsimpledefault102,或simple102(默认选项是default)。
container-class 根据container-type属性,默认的是 Spring 的标准DefaultMessageListenerContainerSimpleMessageListenerContainer
factory-id 使用指定的none将此元素定义的设置公开为JmsListenerContainerFactory,以便它们可以在其他端点上重用。
connection-factory 引用 JMS Bean(默认的 Bean 名称是)。
task-executor 对 Spring TaskExecutor的引用,用于 JMS 侦听器调用程序。
destination-resolver 引用了用于解决 JMSDestination实例的DestinationResolver策略。
message-converter 引用用于将 JMS 消息转换为侦听器
方法参数的MessageConverter策略。默认值是SimpleMessageConverter
error-handler ErrorHandler策略的引用,该策略用于处理在
执行过程中可能发生的MessageListener未捕获的异常。
destination-type 此侦听器的 JMS 目标类型:queuetopicdurableTopicsharedTopic
sharedDurableTopic。这可能启用容器的pubSubDomainsubscriptionDurablesubscriptionShared属性。默认值是queue(禁用
这三个属性)。
response-destination-type 响应的 JMS 目标类型:queuetopic。默认值是destination-type属性的值。
client-id 此侦听器容器的 JMS 客户机 ID。在使用
持久订阅时,必须指定它。
cache JMS 资源的缓存级别:noneconnectionsessionconsumer,或auto。默认情况下(auto),缓存级别实际上是consumer,除非已经指定了外部事务管理器——在这种情况下,有效的
默认值将是none(假设是 Java EE 风格的事务管理,其中给定的
ConnectionFactory 是一个 Xa-aware 池)。
acknowledge 本机 JMS 确认模式:autoclientdups-ok,或transacted。值
transacted激活了局部交易的Session。作为一种选择,你可以指定
transaction-manager属性,稍后将在表中进行说明。默认值为auto
transaction-manager 对外部transaction-manager的引用(通常是基于 XA 的
事务协调器,例如 Spring 的JtaTransactionManager)。如果未指定,则使用
本机确认(请参见acknowledge属性)。
concurrency 为每个侦听器启动的并发会话或消费者的数量。它可以是
表示最大值的简单数字(例如,5),也可以是表示
的下限和上限的范围(例如,3-5)。请注意,指定的最小值只是一个
提示,在运行时可能会被忽略。默认值为1。在
主题侦听器或队列排序很重要的情况下,你应该将并发性限制为1。考虑为
一般队列提高它。
prefetch 要加载到单个会话中的消息的最大数量。请注意,提高这个
值可能会导致并发消费者的饥饿。
receive-timeout 用于接收呼叫的超时(以毫秒为单位)。默认值是1000(一
秒)。-1表示没有超时。
back-off 指定用于计算恢复
尝试之间的间隔的BackOff实例。如果BackOffExecution实现返回BackOffExecution#STOP,则
侦听器容器不再尝试恢复。设置此属性时,将忽略recovery-interval值。默认值是FixedBackOff
之间的间隔为 5000 毫秒(即 5 秒)。
recovery-interval 指定恢复尝试之间的间隔(以毫秒为单位)。它提供了一种方便的Map方法来创建具有指定间隔的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 变体的可用配置选项:

Attribute 说明
factory-id 使用指定的id将此元素定义的设置公开为1,以便它们可以与其他端点一起重用。
resource-adapter 引用 JCAResourceAdapter Bean(默认的 Bean 名称是resourceAdapter)。
activation-spec-factory 引用JmsActivationSpecFactory。默认情况是自动检测 JMS
提供者及其ActivationSpec类(参见[DefaultJmsActivationSpecFactory](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/jms/listener/endpoint/defaultjactivationspecfactory.html))。
destination-resolver 引用DestinationResolver解决 JMSDestinations的策略。
message-converter 引用用于将 JMS 消息转换为侦听器
方法参数的MessageConverter策略。默认值为SimpleMessageConverter
destination-type 此侦听器的 JMS 目标类型:queuetopicdurableTopicsharedTopic
sharedDurableTopic。这可能会启用容器的pubSubDomainsubscriptionDurableActivationSpecsubscriptionShared属性。默认值是queue(禁用JmxTestBean这三个属性)。
response-destination-type 响应的 JMS 目标类型:queuetopic。默认值是
属性的值。
client-id 此侦听器容器的 JMS 客户机 ID。在使用
持久订阅时需要指定它。
acknowledge 本机 JMS 确认模式:autoclientdups-ok,或transacted。一个
的值transacted激活一个局部交易的Session。作为一种选择,你可以指定
后面描述的transaction-manager属性。默认值为auto
transaction-manager 对 Spring JtaTransactionManagerjavax.transaction.TransactionManager的引用,用于为每个
传入消息启动 XA 事务。如果未指定,则使用本机确认(请参见acknowledge属性)。
concurrency 为每个侦听器启动的并发会话或消费者的数量。它可以是
表示最大值的简单数字(例如
),也可以是表示
的下限和上限的范围(例如3-5)。请注意,指定的最小值只是一个
提示,并且在使用 JCA 侦听器容器时,该提示通常在运行时被忽略。
默认值为 1。
prefetch 要加载到单个会话中的消息的最大数量。请注意,提高这个
值可能会导致并发消费者的饥饿。

# 5. JMX

Spring 中的 JMX(Java 管理扩展)支持提供了一些特性,这些特性使你能够轻松、透明地将 Spring 应用程序集成到 JMX 基础设施中。

JMX?

本章不是对 JMX 的介绍。它并不试图解释为什么你可能想要使用 JMX。如果你是 JMX 的新手,请参阅本章末尾的更多资源

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

  • 将任意 Spring Bean 自动注册为 JMX MBean。

  • 一种灵活的机制,用于控制你的 bean 的管理界面。

  • MBean 在远程 JSR-160 连接器上的声明性公开。

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

这些特性被设计成在不将应用程序组件耦合到 Spring 或 JMX 接口和类的情况下工作。实际上,在大多数情况下,你的应用程序类不需要了解 Spring 或 JMX,就可以利用 Spring JMX 特性。

# 5.1.将你的 bean 导出到 JMX

Spring 的 JMX 框架中的核心类是MBeanExporter。这个类负责获取你的 Spring bean 并将它们注册到一个 JMX更多资源。例如,考虑以下类:

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 必须导出到 jmxMBeanServer。在默认配置中,beans``Map中的每个条目的键被用作对应的条目所引用的 Bean 的ObjectName。你可以更改此行为,如[为你的 bean 控制ObjectName实例](#JMX-Naming)中所述。

通过这种配置,testBean Bean 在ObjectName``bean:name=testBean1下作为一个 MBean 公开。默认情况下, Bean 的所有public属性都作为属性公开,所有public方法(除了从Object类继承的方法)都作为操作公开。

MBeanExporter是一个Lifecycle Bean(见启动和关闭回调)。默认情况下,在
应用程序生命周期期间,MBean 会尽可能晚地导出。你可以配置phase
导出,或者通过设置autoStartup标志禁用自动注册。

# 5.1.1.创建一个 mBeanServer

前一节中显示的配置假定应用程序运行在一个已经运行MBeanServer的环境中。在这种情况下, Spring 尝试定位正在运行的MBeanServer,并向该服务器注册你的 bean(如果有的话)。当你的应用程序运行在具有自己的MBeanServer的容器(例如 Tomcat 或 IBMWebSphere)内时,这种行为非常有用。

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

<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创建,并通过MBeanExporter属性提供给MBeanExporter。当你提供自己的MBeanServer实例时,MBeanExporter不会尝试定位正在运行的MBeanServer,而是使用提供的MBeanServer实例。为了正确地实现这一点,你必须在 Classpath 上有一个 JMX 实现。

# 5.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的平台或情况,你应该使用工厂法,如下例所示:

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

# 5.1.3.惰性初始化的 MBean

如果将 Bean 配置为MBeanExporter,该配置也用于延迟初始化,则MBeanExporter不会破坏此契约,并避免实例化 Bean。相反,它使用MBeanServer注册代理,并将从容器获得 Bean 的时间推迟到对代理进行第一次调用时。

# 5.1.4.MBeans 的自动注册

通过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实例](#jmx-naming)中详细说明的那样。

# 5.1.5.控制注册行为

考虑 Spring MBeanExporter通过使用ObjectName``bean:name=testBean1尝试用MBeanServer注册MBean的场景。如果MBean实例已在同一个ObjectName下注册,则默认行为是失败(并抛出InstanceAlreadyExistsException)。

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

Registration behavior 解释
FAIL_ON_EXISTING 这是默认的注册行为。如果MBean实例已经在同一个
下注册了,则正在注册的MBean不是
注册的,则抛出一个InstanceAlreadyExistsException。现有的MBean不受影响。
IGNORE_EXISTING 如果MBean实例已在同一个ObjectName下注册,则正在注册的MBean实例未注册。现有的MBean
不受影响的,并且不会抛出Exception。这在设置
多个应用程序希望在共享的MBeanServer中共享一个公共MBean时很有用。
REPLACE_EXISTING 如果MBean实例已经在相同的ObjectName下注册,则先前注册的
现有的MBean未注册,而新的MBean已在其位置注册(新的MBean有效地替换了
以前的实例)。

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

下面的示例展示了如何从默认注册行为更改为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>

# 5.2.控制 bean 的管理界面

前一节中的示例中,你几乎无法控制 Bean 的管理接口。 Bean 每个导出的
属性和方法都分别作为 JMX 属性和操作公开。 Spring JMX 提供了一种全面的、可扩展的机制,用于控制你的 bean 的管理接口,从而实现对你导出的 bean 的哪些属性和方法作为 JMX 属性和操作实际公开的更精细的控制。

# 5.2.1.使用MBeanInfoAssembler接口

在幕后,
委托给org.springframework.jmx.export.assembler.MBeanInfoAssembler接口的实现,该接口负责定义公开的每个 Bean 的管理接口。默认的实现org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler定义了一个管理接口,该接口公开所有公共属性和方法(如前面几节中的示例所示)。 Spring 提供了MBeanInfoAssembler接口的两个附加实现,它们允许你通过使用源级元数据或任何任意接口来控制生成的管理接口。

# 5.2.2.使用源级元数据:Java 注释

通过使用MetadataMBeanInfoAssembler,你可以通过使用源级元数据来定义 bean 的管理接口。元数据的读取由org.springframework.jmx.export.metadata.JmxAttributeSource接口封装。 Spring JMX 提供了一种使用 Java 注释的默认实现,即org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource。你必须使用JmxAttributeSource接口的实现实例来配置MetadataMBeanInfoAssembler才能使其正常工作(没有默认值)。

要标记用于导出到 JMX 的 Bean,你应该使用ManagedResource注释对 Bean 类进行注释。你必须用ManagedAttribute注释将希望公开的每个方法标记为一个操作,并用ManagedAttribute注释标记希望公开的每个属性。在标记属性时,可以省略 getter 或 setter 的注释,以分别创建只写或只读属性。

一个ManagedResource-带注释的 Bean 必须是公共的,就像公开
一个操作或一个属性的方法一样。

下面的示例显示了我们在创建一个 mBeanServer中使用的JmxTestBean类的注释版本:

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类被标记为ManagedResource注释,并且该ManagedResource注释配置了一组属性。这些属性可用于配置由MBeanExporter生成的 MBean 的各个方面,并在后面的源级元数据类型中进行更详细的说明。

agename属性都使用JmxTestBean注释,但是,在age属性的情况下,只标记 getter。这导致这两个属性都作为属性包含在管理接口中,但是age属性是只读的。

最后,add(int, int)方法被标记为ManagedOperation属性,而dontExposeMe()方法则不是。当你使用MetadataMBeanInfoAssembler时,这将导致管理接口仅包含一个操作(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 已配置了KeyNamingStrategy类的实例,并通过 Assembler 属性传递给MBeanExporter。这就是为暴露在 Spring 中的 MBean 利用元数据驱动的管理接口所需的全部内容。

# 5.2.3.源级元数据类型

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

Purpose 注释 Annotation Type
Mark all instances of a Class as JMX managed resources. @ManagedResource Class
Mark a method as a JMX operation. @ManagedOperation Method
Mark a getter or setter as one half of a JMX attribute. @ManagedAttribute Method (only getters and setters)
Define descriptions for operation parameters. @ManagedOperationParameter@ManagedOperationParameters Method

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

Parameter Description 适用于
ObjectName Used by MetadataNamingStrategy to determine the ObjectName of a managed resource. ManagedResource
description Sets the friendly description of the resource, attribute or operation. ManagedResourceManagedAttributeManagedOperation,或ManagedOperationParameter
currencyTimeLimit Sets the value of the currencyTimeLimit descriptor field. ManagedResourceManagedAttribute
defaultValue Sets the value of the defaultValue descriptor field. ManagedAttribute
log Sets the value of the log descriptor field. ManagedResource
logFile Sets the value of the logFile descriptor field. ManagedResource
persistPolicy Sets the value of the persistPolicy descriptor field. ManagedResource
persistPeriod Sets the value of the persistPeriod descriptor field. ManagedResource
persistLocation Sets the value of the persistLocation descriptor field. ManagedResource
persistName Sets the value of the persistName descriptor field. ManagedResource
name Sets the display name of an operation parameter. ManagedOperationParameter
index Sets the index of an operation parameter. ManagedOperationParameter

# 5.2.4.使用AutodetectCapableMBeanInfoAssembler接口

为了进一步简化配置, Spring 包括AutodetectCapableMBeanInfoAssembler接口,该接口扩展了MBeanInfoAssembler接口,以添加对 MBean 资源的自动检测的支持。如果你使用MBeanExporter的实例配置AutodetectCapableMBeanInfoAssembler,则允许对包含用于暴露于 JMX 的 bean 进行“投票”。

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>

注意,在前面的配置中,没有向MBeanExporter传递任何 bean。但是,JmxTestBean仍然是注册的,因为它被标记为ManagedResource属性,并且MetadataMBeanInfoAssembler检测到并投票将其包括在内。这种方法的唯一问题是,JmxTestBean的名称现在具有业务含义。你可以通过更改在[为你的 bean 控制ObjectName实例]中定义的ObjectName创建的默认行为来解决此问题。

# 5.2.5.使用 Java 接口定义管理接口

除了MetadataMBeanInfoAssembler, Spring 还包括InterfaceBasedMBeanInfoAssembler,它允许你约束基于接口集合中定义的一组方法公开的方法和属性。

尽管公开 MBean 的标准机制是使用接口和简单的命名方案,但InterfaceBasedMBeanInfoAssembler通过消除对命名约定的需求,允许你使用多个接口,并消除对你的 Bean 实现 MBean 接口的需求,从而扩展了该功能。

考虑下面的接口,它用于为JmxTestBean类定义一个管理接口,我们在前面展示了这个接口:

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 以使用此接口作为管理接口的定义:

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

在前面的示例中,InterfaceBasedMBeanInfoAssembler被配置为在为任何 Bean 构建管理接口时使用IJmxTestBean接口。理解由InterfaceBasedMBeanInfoAssembler处理的 bean 不需要实现用于生成 JMX 管理接口的接口是很重要的。

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

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

# 5.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 操作公开,ObjectNamesetName(String)getAge()作为 JMX 属性的适当的一半公开。在前面的代码中,方法映射应用于公开给 JMX 的 bean。要在 Bean-by- Bean 的基础上控制方法曝光,可以使用methodMappingsMethodNameMBeanInfoAssembler属性将 Bean 名称映射到方法名称列表。

# 5.3.控制 bean 的ObjectName实例

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

# 5.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实例,该实例是从映射属性定义的Properties实例和映射属性定义的路径中的属性文件合并而来的。在这种配置中,testBean Bean 给定了ObjectNameObjectName,因为这是@ManagedOperationParameters实例中的条目,该实例具有与 Bean 键对应的键。

如果在Properties实例中找不到条目,则 Bean 键名被用作ObjectName

# 5.3.2.使用MetadataNamingStrategy

MetadataNamingStrategy在每个 Bean 上使用objectName属性的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>

如果没有为objectName属性提供ManagedResource,则使用以下格式创建ObjectName:[fully-qualified-package-name]:type=[short-classname],name=[bean-name]。例如,以下 Bean 生成的ObjectName将是com.example:type=MyClass,name=myBean:

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

# 5.3.3.配置基于注释的 MBean 导出

如果你更喜欢使用基于注释的方法来定义管理接口,那么可以使用MBeanExporter的方便子类:AnnotationMBeanExporter。在定义这个子类的实例时,你不再需要namingStrategyassemblerattributeSource配置,因为它总是使用标准的基于 Java 注释的元数据(也总是启用自动检测)。实际上,与定义MBeanExporter Bean 不同的是,@EnableMBeanExport``@Configuration注释支持更简单的语法,如下例所示:

@Configuration
@EnableMBeanExport
public class AppConfig {

}

如果你更喜欢基于 XML 的配置,则<context:mbean-export/>元素具有相同的目的,如以下清单所示:

<context:mbean-export/>

如果有必要,可以提供对特定 MBeanserver的引用,并且defaultDomain属性(属性为AnnotationMBeanExporter)接受生成的 MBeanObjectName域的备选值。正如下面的示例所示,正如在MetadatanamingStrategy上一节中所描述的那样,这将代替完全限定的包名:

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

}

下面的示例展示了前面的基于注释的示例的 XML 等价物:

<context:mbean-export server="myMBeanServer" default-domain="myDomain"/>
不要在 Bean 类中结合自动检测 JMX
注释使用基于接口的 AOP 代理。基于接口的代理“隐藏”目标类,它
还隐藏 JMX 管理的资源注释。因此,你应该在
的情况下使用目标类代理(通过在<aop:config/><tx:annotation-driven/>上设置“proxy-target-class”标志,以此类推)。否则,在
启动时,你的 JMX Bean 可能会被忽略。

# 5.4.使用 JSR-160 连接器

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

# 5.4.1.服务器端连接器

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

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

默认情况下,ConnectorServerFactoryBean创建一个JMXConnectorServer绑定到service:jmx:jmxmp://localhost:9875。因此,serverConnector Bean 通过 LocalHost 上的 JMXMP 协议(端口 9875)将本地MBeanServer公开给客户端。请注意,JMXMP 协议在 JSR160 规范中被标记为可选的。目前,主要的开源 JMX 实现 MX4J 和 JDK 提供的 JDJ 都不支持 JMXMP。

要指定另一个 URL 并将JMXConnectorServer本身注册为MBeanServer,你可以分别使用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>

# 5.4.2.客户端连接器

要创建到远程 JSR-160 启用的MBeanServerConnectionMBeanServerMBeanServerConnectionFactoryBean,你可以使用MBeanServerConnectionFactoryBean,如下例所示:

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

# 5.4.3.JMX 超过 Hessian 或 SOAP

JSR-160 允许扩展客户机和服务器之间的通信方式。前面几节中所示的示例使用了 JSR-160 规范(IIOP 和 JRMP)和(可选的)JMXMP 所要求的基于 RMI 的强制实现。通过使用其他提供者或 JMX 实现(例如MX4J (opens new window)),你可以在简单的 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>

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

# 5.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属性控制,将这些接口上的方法和属性映射到 MBean 上的操作和属性的规则与InterfaceBasedMBeanInfoAssembler使用的规则相同。

MBeanProxyFactoryBean可以创建通过MBeanServerConnection可访问的任何 MBean 的代理。默认情况下,定位并使用了本地MBeanServer,但是可以重写此内容并提供指向远程MBeanServerConnectionMBeanServerConnection,以满足指向远程 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,它指向使用MBeanServerConnectionFactoryBean的远程计算机。然后通过server属性将此MBeanServerConnection传递给MBeanProxyFactoryBean。创建的代理通过MBeanServerConnection将所有调用转发到MBeanServer

# 5.6.通知

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

# 5.6.1.为通知注册监听器

Spring 的 JMX 支持使得在任意数量的 MBean 中注册任意数量的NotificationListeners变得很容易(这包括由 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)广播一个 JMXNotification时,就会通知通过bean:name=testBean1属性注册为侦听器的ConsoleLoggingNotificationListener Bean。然后,ConsoleLoggingNotificationListener Bean 可以对Notification采取它认为适当的任何操作。

还可以使用 straight Bean names 作为导出 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属性映射中一个条目的键,如下例所示:

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

如果需要执行逆操作(即针对 MBean 注册多个不同的侦听器),则必须使用notificationListenerslist 属性(优先于notificationListenerMappings属性)。这一次,我们不再为单个 MBean 配置NotificationListener实例,而是配置NotificationListenerBean实例。aNotificationListenerBean封装了将在MBeanServer中注册的ObjectName(或ObjectNames)。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时都给出一个 handback 对象,并且我们还希望通过提供Notifications来过滤掉无关的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>

(关于什么是 handback 对象以及NotificationFilter是什么的详细讨论,请参见 JMX 规范(1.2)中题为“JMX 通知模型”的部分。

# 5.6.2.发布通知

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

这一部分实际上只与 Spring 管理的 bean 相关,这些 bean 的
通过MBeanExporter被公开为 MBean。任何现有的用户定义的 MBean 都应该
使用标准的 JMXAPI 进行通知发布。

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

正如在[NotificationPublisher](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/jmx/export/NotificationPublisher.html)接口的 Javadoc 中所述,通过NotificationPublisher机制发布事件的托管 bean 不负责通知侦听器的状态管理。 Spring 的 JMX 支持负责处理所有的 JMX 基础设施问题。作为应用程序开发人员,你所需要做的就是实现NotificationPublisherAware接口,并通过使用提供的NotificationPublisher实例开始发布事件。请注意,NotificationPublisher是在托管 Bean 已注册为MBeanServer之后设置的。

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

在下面的示例中,每次调用JmxTestBean操作时,导出的NotificationEvent实例都会发布一个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 的耦合,那么就这样做。

# 5.7.更多资源

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

# 6. 电子邮件

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

库依赖项

为了使用 Spring 框架的电子邮件库,需要在你的应用程序的 Classpath 上设置以下 jar:

这个库可以在 Web 上免费获得——例如,在 Maven Central 中为com.sun.mail:jakarta.mail。请确保使用最新的 1.6.x 版本,而不是 Jakarta Mail2.0(它带有不同的包名称空间)。

Spring 框架提供了用于发送电子邮件的有用的工具库,该工具库保护你不受底层邮件系统的详细信息的影响,并代表客户机负责低级别的资源处理。

org.springframework.mail包是 Spring 框架电子邮件支持的根级别包。发送电子邮件的中心接口是MailSender接口。一个简单的值对象封装了一个简单邮件的属性,如fromto(加上许多其他的)是SimpleMailMessage类。这个包还包含一个检查异常的层次结构,它们提供了比较低级别邮件系统异常更高级别的抽象,根异常是MailException。有关富邮件异常层次结构的更多信息,请参见javadoc (opens new window)

org.springframework.mail.javamail.JavaMailSender接口增加了专门的 JavaMail 功能,例如对MailSender接口的 MIME 消息支持(它从该接口继承而来)。JavaMailSender还提供了一个名为org.springframework.mail.javamail.MimeMessagePreparator的回调接口,用于准备MimeMessage

# 6.1.用法

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

public interface OrderManager {

    void placeOrder(Order order);

}

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

# 6.1.1.基本MailSenderSimpleMailMessage用法

下面的示例展示了如何在有人下订单时使用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.example"/>
</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="[email protected]"/>
    <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.使用JavaMailSenderMimeMessagePreparator

本节描述OrderManager的另一个实现,它使用MimeMessagePreparator回调接口。在下面的示例中,mailSender属性的类型为JavaMailSender,这样我们就能够使用 JavaMailMimeMessage类:

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("[email protected]"));
                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());
        }
    }

}
邮件代码是一个横切关注点,很可能是
重构为custom Spring AOP aspect的候选项,然后在
目标上的适当接入点上运行OrderManager

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

# 6.2.使用 JavaMailMimeMessageHelper

在处理 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("[email protected]");
helper.setText("Thank you for ordering!");

sender.send(message);

# 6.2.1.发送附件和内联资源

多部分电子邮件消息允许使用附件和内联资源。内联资源的示例包括希望在消息中使用但不希望显示为附件的图像或样式表。

# 附件

下面的示例向你展示了如何使用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("[email protected]");

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);
# 内联资源

下面的示例向你展示了如何使用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("[email protected]");

// 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);
通过使用上述示例中指定的Content-IDidentifier1234),将内联资源添加到MimeMessage中。添加文本
的顺序和资源是非常重要的。一定要先添加文本,然后
资源。如果你是在做相反的方式,它是不起作用的。

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

前面几节所示示例中的代码通过使用message.setText(..)等方法调用,显式地创建了电子邮件消息的内容。这对于简单的情况很好,在前面提到的示例的上下文中也是可以的,其目的是向你展示 API 的基本知识。

然而,在典型的 Enterprise 应用程序中,开发人员通常不会使用前面所示的方法来创建电子邮件消息的内容,原因有很多:

  • 在 Java 代码中创建基于 HTML 的电子邮件内容是乏味且容易出错的。

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

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

通常,解决这些问题的方法是使用模板库(例如 Freemarker)来定义电子邮件内容的显示结构。这将使你的代码只负责创建要在电子邮件模板中呈现的数据并发送电子邮件。当你的电子邮件内容变得相当复杂时,这绝对是一种最佳实践,而且,有了 Spring 框架对 Freemarker 的支持类,这变得非常容易做到。

# 7. 任务执行和调度

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

Spring 集成类还具有支持调度的Timer(自 1.3 起的 JDK 的一部分)和 Quartz 调度器(https://www.quartz-scheduler.org/ (opens new window))。你可以通过使用FactoryBean以及可选引用TimerTrigger实例来分别设置这两个调度程序。此外,Quartz 调度器和Timer都有一个方便的类,它允许你调用现有目标对象的方法(类似于正常的MethodInvokingFactoryBean操作)。

# 7.1. Spring TaskExecutor抽象

执行器是线程池概念的 JDK 名称。之所以命名为“executor”,是因为不能保证底层实现实际上是一个池。执行器可以是单线程的,甚至可以是同步的。 Spring 的抽象隐藏了 Java SE 和 Java EE 环境之间的实现细节。

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

创建TaskExecutor最初是为了在需要时为其他 Spring 组件提供线程池的抽象。诸如ApplicationEventMulticaster、JMS 的AbstractMessageListenerContainer和 Quartz Integration 等组件都使用TaskExecutor抽象来池线程。但是,如果你的 bean 需要线程池行为,你也可以根据自己的需要使用此抽象。

# 7.1.1.TaskExecutor类型

Spring 包括许多预构建TaskExecutor的实现方式。十有八九,你应该永远不需要实现你自己的。 Spring 提供的备选案文如下:

  • SyncTaskExecutor:此实现不异步运行调用。相反,每次调用都发生在调用线程中。它主要用于不需要多线程的情况,例如在简单的测试用例中。

  • SimpleAsyncTaskExecutor:此实现不重用任何线程。相反,它为每个调用启动一个新线程。但是,它确实支持一个并发限制,该限制可以阻止任何超出限制的调用,直到释放了一个插槽。如果你正在寻找真正的池,请参阅下面的列表中的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:该实现使用 CommonJWorkManager作为其支持服务提供者,并且是在 Spring 应用程序上下文中在 WebLogic 或 WebSphere 上设置基于 CommonJ 的线程池集成的中心便利类。

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

# 7.1.2.使用TaskExecutor

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

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的各种方法,用于调度在将来的某个时刻运行的任务。下面的清单显示了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的方法,它只需要一个Runnable和一个Date。这会导致任务在指定的时间之后运行一次。所有其他方法都能够调度任务以重复运行。固定速率和固定延迟方法用于简单的周期性执行,但是接受Trigger的方法要灵活得多。

# 7.2.1.Trigger接口

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

public interface Trigger {

    Date nextExecutionTime(TriggerContext triggerContext);
}

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

public interface TriggerContext {

    Date lastScheduledExecutionTime();

    Date lastActualExecutionTime();

    Date lastCompletionTime();
}

# 7.2.2.Trigger实现

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 提供了在 WebLogic 或 WebSphere 上委托给 CommonJ 的,以及在 Java EE7+ 环境中委托给 JSR-236的较新的。两者通常都配置了 JNDI 查找。

每当不需要外部线程管理时,一种更简单的选择是在应用程序内进行本地ScheduledExecutorService设置,可以通过 Spring 的ConcurrentTaskScheduler进行调整。为了方便起见, Spring 还提供了ThreadPoolTaskScheduler,它在内部委托给ScheduledExecutorService,以便沿着ThreadPoolTaskExecutor的行提供通用 Bean 样式的配置。这些变体在宽松的应用程序服务器环境中——特别是在 Tomcat 和 Jetty 上——的本地嵌入线程池设置中也非常好用。

# 7.3.对调度和异步执行的注释支持

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

# 7.3.1.启用调度注释

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

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

你可以为你的应用程序选择相关的注释。例如,如果只需要支持@Scheduled,则可以省略@EnableAsync。对于更细粒度的控件,你可以另外实现SchedulingConfigurer接口、AsyncConfigurer接口,或者两者兼有。参见[SchedulingConfigurer](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/scheductation/annotation/schedulingconfigurer.html)和[AsyncConfigurer(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/asyncframework/annotation/annotation/javador.html/javad

如果你更喜欢 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,提供了一个 Executor 引用来处理那些与@Async注释的方法对应的任务,并且提供了一个 Scheduler 引用来管理那些用@Scheduled注释的方法。

用于处理@Async注释的默认通知模式是proxy,它仅允许
通过代理拦截调用。同一类
中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,可以考虑将
转换为aspectj模式,并结合编译时或加载时编织。

# 7.3.2.@Scheduled注释

你可以将@Scheduled注释以及触发器元数据添加到方法中。例如,以下方法每 5 秒(5000 毫秒)以固定的延迟被调用一次,这意味着该周期是从之前每次调用的完成时间开始计算的。

@Scheduled(fixedDelay = 5000)
public void doSomething() {
    // something that should run periodically
}
默认情况下,毫秒将被用作固定延迟、固定速率和
初始延迟值的时间单位。如果你想使用不同的时间单位,例如秒或
分钟,你可以通过timeUnit中的timeUnit属性来配置这一点。

例如,前面的示例也可以写成如下。

如果需要固定速率执行,可以在注释中使用fixedRate属性。以下方法每五秒调用一次(在每次调用的连续启动时间之间进行度量)。

@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
    // something that should run periodically
}

对于固定延迟和固定速率任务,你可以通过指示在第一次执行该方法之前等待的时间量来指定初始延迟,如下fixedRate示例所示。

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

如果简单的周期性调度不够表达,则可以提供CRON 表达。以下示例仅在工作日运行:

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
    // something that should run on weekdays only
}
还可以使用zone属性指定解析 CRON
表达式的时区。

请注意,要调度的方法必须具有 void 返回,并且不能接受任何参数。如果方法需要与来自应用程序上下文的其他对象交互,那么这些对象通常是通过依赖注入提供的。

在 Spring Framework4.3 中,@Scheduled方法在任何作用域的 bean 上都是受支持的。

请确保你不是在运行时初始化同一个@Scheduled注释类的多个实例,除非你确实希望将回调安排到每个这样的
实例。与此相关,请确保在 Bean
上不使用@Configurable@Scheduled注释并在容器中注册为常规 Spring bean
的类。否则,你将获得双重初始化(一次通过
容器,一次通过@Configurable方面),结果是每个@Scheduled方法被调用两次。

# 7.3.3.@Async注释

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

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

与使用@Scheduled注释的方法不同,这些方法可以预期参数,因为它们在运行时由调用者以“正常”方式调用,而不是从容器管理的计划任务中调用。例如,以下代码是@Async注释的合法应用程序:

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

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

@Async
Future<String> returnSomething(int i) {
    // this will be run asynchronously
}
@Async方法不仅可以声明一个常规的java.util.concurrent.Future返回类型
,还可以声明 Spring 的org.springframework.util.concurrent.ListenableFuture,或者,如 Spring
4.2,JDK8 的java.util.concurrent.CompletableFuture,用于与
异步任务进行更丰富的交互,并用于通过进一步的处理步骤立即组合。

不能将@Async与生命周期回调结合使用,例如@PostConstruct。要异步初始化 Spring bean,你目前必须使用一个单独的初始化 Spring Bean,然后调用目标上的@Async注释方法,如下例所示:

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

}
对于@Async没有直接的 XML 等价物,因为这样的方法首先应该为异步执行设计
,而不是在外部重新声明为异步。
但是,你可以手动设置 Spring 的AsyncExecutionInterceptor与 Spring AOP,
结合自定义切入点。

# 7.3.4.带有@Async的遗嘱执行人资格

默认情况下,在方法上指定@Async时,使用的执行器是启用异步支持时进行配置,即“注释驱动”元素(如果你使用 XML 或AsyncConfigurer实现)。但是,当需要指示在执行给定方法时应该使用默认值以外的执行器时,可以使用value注释的@Async属性。下面的示例展示了如何做到这一点:

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

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

# 7.3.5.异常管理与@Async

@Async方法具有Future类型的返回值时,很容易管理在方法执行期间抛出的异常,因为在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.task名称空间

在版本 3.0 中, Spring 包括一个用于配置TaskExecutorTaskScheduler实例的 XML 命名空间。它还提供了一种方便的方式来配置要用触发器调度的任务。

# 7.4.1.“调度程序”元素

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

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

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

# 7.4.2.executor元素

下面创建一个ThreadPoolTaskExecutor实例:

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

上一节中显示的调度程序一样,为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]的文档(https://DOCS.oracle.com/javase/8/DOCS/api/java/util/concurrent/threadpoolexecutor.html)。其主要思想是,当提交任务时,如果当前活动线程的数量小于核心大小,则执行器首先尝试使用空闲线程。如果已达到核心大小,则将该任务添加到队列中,只要该任务的容量尚未达到。只有这样,如果队列的容量已经达到,执行器才会创建超出核心大小的新线程。如果也达到了最大大小,那么执行器将拒绝该任务。

默认情况下,队列是无界的,但这很少是所需的配置,因为如果在所有池线程都忙的时候向队列中添加了足够多的任务,则可能导致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设置决定了线程在停止之前可以保持空闲的时间限制(以秒为单位)。如果池中的线程数量超过了当前的核心数量,那么在不处理任务的情况下等待了这么长的时间之后,多余的线程将被停止。时间值为零会导致多余的线程在执行任务后立即停止,而不会在任务队列中保留后续工作。下面的示例将keep-alive值设置为两分钟:

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

# 7.4.3.“计划任务”元素

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

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

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

调度程序由外部元素引用,每个单独的任务都包括其触发器元数据的配置。在前面的示例中,该元数据定义了一个具有固定延迟的周期性触发器,该延迟指示每个任务执行完成后要等待的毫秒数。另一个选项是fixed-rate,指示无论之前的执行需要多长时间,该方法应该运行多长时间。此外,对于fixed-delayfixed-rate任务,都可以指定一个“initial-delay”参数,该参数指示在第一次执行该方法之前需要等待的毫秒数。对于更多的控制,你可以提供cron属性来提供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.CRON 表达式

所有 Spring CRON 表达式都必须符合相同的格式,无论你是在[@Scheduled注释](# 调度-注释-支持-调度)、[task:scheduled-tasks元素](# 调度-任务-命名空间-调度-任务)中使用它们,还是在其他地方使用它们。格式良好的 CRON 表达式,如* * * * * *,由六个以空间分隔的时间和日期字段组成,每个字段都有自己的有效值范围:

 ┌───────────── second (0-59)
 │ ┌───────────── minute (0 - 59)
 │ │ ┌───────────── hour (0 - 23)
 │ │ │ ┌───────────── day of the month (1 - 31)
 │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
 │ │ │ │ │ ┌───────────── day of the week (0 - 7)
 │ │ │ │ │ │          (0 or 7 is Sunday, or MON-SUN)
 │ │ │ │ │ │
 * * * * * *

有一些规则是适用的:

  • 字段可以是星号(*),它总是代表“first-last”。对于月中日或周中日字段,可以使用问号(?)来代替星号。

  • 逗号(,)用于分隔列表中的项。

  • 用连字符分隔的两个数字(-)表示一系列数字。指定的范围是包含的。

  • /的范围(或*)之后,用/指定该数值在该范围内的值的间隔。

  • 英文名称也可以用于月日和周日域。使用特定日期或月份的前三个字母(大小写无关紧要)。

  • 月中日和周中日字段可以包含L字符,该字符具有不同的含义

    • 在月日字段中,L表示这个月的最后一天。如果后面跟着一个负偏移量(即L-n),则表示 *n当月倒数第二天 *。

    • 在一周中,L代表一周的最后一天。如果前缀是数字或三个字母的名称(dLDDDL),则表示 * 月份中的最后一天(dDDD)。

  • 月中日字段可以是nW,代表 * 最近的工作日到一个月中的一天n。如果n在周六下跌,这将产生它之前的周五。如果n在周日下跌,这将产生之后的星期一,如果n1并在星期六下跌,也会发生这种情况(即:1W代表这个月的第一个工作日。*)。

  • 如果月日字段是LW,则表示这个月的最后一个工作日

  • 周日域可以是d#n(或DDD#n),表示 * 周的第 * 天n(或DDD)。

以下是一些例子:

Cron Expression 意义
0 0 * * * * 每天的每个小时都是最重要的
*/10 * * * * * 每十秒钟
0 0 8-10 * * * 每天 8 点、9 点和 10 点
0 0 6,19 * * * 每天早上 6 点和晚上 7 点
0 0/30 8-10 * * * 每天 8:00、8:30、9:00、9:30、10:00 和 10:30
0 0 9-17 * * MON-FRI 工作日朝九晚五的时候
0 0 0 25 DEC ? 每个圣诞节的午夜
0 0 0 L * * 每月的最后一天午夜
0 0 0 L-3 * * 本月倒数第三天午夜
0 0 0 * * 5L 每月的最后一个星期五午夜
0 0 0 * * THUL 每月的最后一个星期四午夜
0 0 0 1W * * 每月的第一个工作日午夜
0 0 0 LW * * 每月最后一个工作日的午夜
0 0 0 ? * 5#2 本月的第二个星期五午夜。
0 0 0 ? * MON#1 这个月的第一个星期一午夜。

# 7.5.1.宏

对于人类来说,0 0 * * * *这样的表达式很难解析,因此在出现错误的情况下很难修复。 Spring 为了提高可读性,支持以下宏,它们表示常用的序列。你可以使用这些宏而不是六位数的值,因此:@Scheduled(cron = "@hourly")

Macro 意义
@yearly (or @annually) 一年一次(0 0 0 1 1 *
@monthly 每月一次(0 0 0 1 * *
@weekly 每周一次(0 0 0 * * 0
@daily (or @midnight) 一天一次(0 0 0 * * *),或
@hourly 每小时一次,(0 0 * * * *

# 7.6.使用 Quartz 调度器

Quartz 使用TriggerJobJobDetail对象来实现对各类作业的调度。关于石英背后的基本概念,见https://www.quartz-scheduler.org/ (opens new window)。出于方便的目的, Spring 提供了几个类,这些类简化了基于 Spring 的应用程序中使用 Quartz 的过程。

# 7.6.1.使用JobDetailFactoryBean

QuartzJobDetail对象包含运行作业所需的所有信息。 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)。超时在作业数据图中指定。作业数据映射可以通过JobExecutionContext(在执行时传递给你)获得,但是JobDetail还可以从映射到作业实例的属性的作业数据获取其属性。因此,在下面的示例中,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
    }
}

你也可以使用作业数据图中的所有附加属性。

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

# 7.6.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 作业是无状态的,这会导致作业相互干扰的可能性。如果为同一个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>
默认情况下,作业将以并发方式运行。

# 7.6.3.通过使用触发器和SchedulerFactoryBean连接作业

我们创造了工作细节和工作岗位。 Bean 我们还介绍了使你能够在特定对象上调用方法的便利性。当然,我们仍然需要自己安排工作。这是通过使用触发器和SchedulerFactoryBean来完成的。Quartz 中有几个触发器, Spring 提供了两个 QuartzFactoryBean实现,它们具有方便的默认值:CronTriggerFactoryBeanSimpleTriggerFactoryBean

需要对触发器进行计划。 Spring 提供了一个SchedulerFactoryBean,它公开了要设置为属性的触发器。SchedulerFactoryBean用这些触发器调度实际的作业。

下面的列表使用了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,有更多的属性可用,例如作业详细信息使用的日历、用来自定义 Quartz 的属性,以及 Spring 提供的 JDBC 数据源。有关更多信息,请参见[SchedulerFactoryBean](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/scheducting/quartz/schedulerfactorybean.html)Javadoc。

SchedulerFactoryBean还可以识别 Classpath 中的quartz.properties文件,
基于石英属性键,就像常规的石英配置一样。请注意,许多SchedulerFactoryBean设置与属性文件中的常见石英设置交互;因此,不建议在这两个级别上指定值。例如,如果你打算依赖 Spring 提供的数据源,请不要设置
“org.quartz.jobstore.class”属性,
或指定org.springframework.scheduling.quartz.LocalDataSourceJobStore变体,其
是标准org.quartz.impl.jdbcjobstore.JobStoreTX的完全替代。

# 8. 缓存抽象

自版本 3.1 以来, Spring 框架提供了对向现有 Spring 应用程序透明地添加缓存的支持。与交易支持类似,缓存抽象允许一致地使用各种缓存解决方案,并且对代码的影响最小。

在 Spring Framework4.1 中,缓存抽象得到了显著扩展,支持JSR-107 注释和更多的自定义选项。

# 8.1.理解缓存抽象

缓存 VS 缓冲区

“缓冲”和“缓存”这两个词往往可以互换使用。然而,请注意,它们代表的是不同的东西。传统上,缓冲区被用作快速实体和慢速实体之间的数据的中间临时存储。由于一方将不得不等待另一方(这会影响性能),缓冲区通过允许整个数据块同时移动而不是在小块中移动来缓解这种情况。数据只从缓冲区写入和读取一次。此外,至少有一方意识到缓冲区是可见的。

另一方面,根据定义,缓存是隐藏的,并且双方都不知道缓存的发生。它也提高了性能,但这是通过让相同的数据以快速的方式被多次读取来实现的。

你可以找到对缓冲区和缓存之间的差异的进一步解释[here](https://en.wikipedia.org/wiki/Cache_(computing)#the_difference_between_buffer_and_cache)。

在其核心部分,缓存抽象将缓存应用于 Java 方法,从而减少了基于缓存中可用信息的执行次数。也就是说,每次调用目标方法时,抽象都会应用一种缓存行为,该行为会检查该方法是否已经针对给定参数被调用了。如果它已被调用,则将返回缓存的结果,而无需调用实际的方法。如果该方法未被调用,则调用该方法,并将结果缓存并返回给用户,这样,下一次调用该方法时,将返回缓存的结果。通过这种方式,对于给定的一组参数,昂贵的方法(不管是 CPU-还是 IO-bound)只能调用一次,并且结果可以重用,而无需再次实际调用该方法。缓存逻辑是透明地应用的,不会对调用者造成任何干扰。

这种方法仅适用于那些被保证为给定输入(或参数)返回相同的
输出(结果)的方法,无论调用它多少次。

缓存抽象提供了其他与缓存相关的操作,例如更新缓存内容或删除一个或所有条目的能力。如果缓存处理的是在应用程序运行过程中可能发生变化的数据,那么这些就很有用。

与 Spring 框架中的其他服务一样,缓存服务是一种抽象(而不是缓存实现),需要使用实际存储来存储缓存数据——也就是说,该抽象使你不必编写缓存逻辑,但并不提供实际的数据存储。这个抽象是通过org.springframework.cache.Cacheorg.springframework.cache.CacheManager接口实现的。

Spring 提供了该抽象的几个实现:基于 JDKjava.util.concurrent.ConcurrentMap的缓存、Ehcache2.x (opens new window)、Gemfire 缓存、Caffeine (opens new window),以及与 JSR-107 兼容的缓存(例如 EHCache3.x)。有关插入其他缓存存储和提供程序的更多信息,请参见插入不同的后端缓存

对于多线程和
多进程环境,缓存抽象没有特殊的处理,因为这些特性由缓存实现处理。

如果你有一个多进程环境(即部署在多个节点上的应用程序),则需要相应地配置你的缓存提供程序。根据你的用例,在几个节点上复制相同的数据就足够了。但是,如果在应用程序运行过程中更改了数据,则可能需要启用其他传播机制。

缓存特定的项目直接等同于在编程缓存交互中发现的典型的“如果没有找到,就继续进行并最终放置”的代码块。没有应用锁,并且多个线程可能会尝试并发加载相同的项。这同样适用于驱逐。如果多个线程试图同时更新或删除数据,则可能会使用过时的数据。某些缓存提供商在该领域提供了高级功能。有关更多详细信息,请参见缓存提供程序的文档。

要使用缓存抽象,你需要注意两个方面:

  • 缓存声明:确定需要缓存的方法及其策略。

  • 缓存配置:存储数据并从中读取数据的备份缓存。

# 8.2.基于声明性注释的缓存

对于缓存声明, Spring 的缓存抽象提供了一组 Java 注释:

  • @Cacheable:触发缓存填充。

  • @CacheEvict:触发缓存驱逐。

  • @CachePut:在不干扰方法执行的情况下更新缓存。

  • @Caching:重新组合要在方法上应用的多个缓存操作。

  • @CacheConfig:在类级别上共享一些常见的缓存相关设置。

# 8.2.1.@Cacheable注释

顾名思义,你可以使用@Cacheable来划分可缓存的方法——即,将结果存储在缓存中的方法,以便在随后的调用中(使用相同的参数),将返回缓存中的值,而无需实际调用该方法。在最简单的形式中,注释声明需要与注释方法关联的缓存的名称,如下例所示:

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

在前面的代码片段中,findBook方法与名为books的缓存相关联。每次调用该方法时,都会检查缓存,以查看调用是否已经运行,并且不需要重复调用。虽然在大多数情况下,只声明一个缓存,但该注释允许指定多个名称,以便使用多个缓存。在这种情况下,每个缓存都会在调用方法之前进行检查——如果至少有一个缓存被命中,则会返回相关值。

所有其他不包含该值的缓存也会被更新,即使
缓存的方法实际上没有被调用。

下面的示例在带有多个缓存的findBook方法上使用@Cacheable:

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
# 默认密钥生成

由于缓存本质上是键值存储,因此缓存方法的每次调用都需要转换为合适的键,以便进行缓存访问。缓存抽象使用基于以下算法的简单KeyGenerator:

  • 如果没有给出参数,则返回SimpleKey.EMPTY

  • 如果只给出一个参数,则返回该实例。

  • 如果给出了一个以上的参数,则返回一个包含所有参数的SimpleKey

只要参数具有自然键,并且实现有效的hashCode()equals()方法,这种方法在大多数用例中都能很好地工作。如果不是这样,你就需要改变策略。

要提供不同的默认密钥生成器,你需要实现org.springframework.cache.interceptor.KeyGenerator接口。

Spring 4.0 版本的发布改变了默认的密钥生成策略。 Spring 的早期
版本使用了一种密钥生成策略,对于多个密钥参数,
只考虑参数的hashCode(),而不是equals()。这可能会导致
意外的密钥冲突(有关背景信息,请参见SPR-10237 (opens new window))。新的SimpleKeyGenerator在这种情况下使用复合键。

如果你想继续使用以前的键策略,可以配置不推荐的org.springframework.cache.interceptor.DefaultKeyGenerator类,或者创建一个自定义的
基于散列的KeyGenerator实现。
# 自定义密钥生成声明

由于缓存是通用的,目标方法很可能具有各种签名,而这些签名不能很容易地映射到缓存结构的顶部。当目标方法有多个参数时,这一点往往变得很明显,其中只有一些参数适合缓存(而其余的参数仅由方法逻辑使用)。考虑以下示例:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

乍一看,虽然两个boolean参数会影响找到这本书的方式,但它们对缓存没有用处。此外,如果这两个因素中只有一个是重要的,而另一个不是重要的,那该怎么办?

对于这种情况,@Cacheable注释允许你指定如何通过其key属性生成密钥。你可以使用SpEL来选择感兴趣的参数(或它们的嵌套属性),执行操作,甚至调用任意方法,而无需编写任何代码或实现任何接口。这是在默认生成器上推荐的方法,因为随着代码库的增长,签名中的方法往往会有很大的不同。虽然默认策略可能对某些方法有效,但它很少对所有方法有效。

以下示例使用了各种 SPEL 声明(如果你不熟悉 SPEL,请帮自己一个忙,并阅读Spring Expression Language):

@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)
keykeyGenerator参数是互斥的,指定这两个参数的操作
会导致异常。
# 默认缓存分辨率

缓存抽象使用一个简单的CacheResolver,该抽象使用配置的CacheManager检索在操作级别上定义的缓存。

要提供不同的默认缓存解析器,你需要实现org.springframework.cache.interceptor.CacheResolver接口。

# 自定义缓存分辨率

默认的缓存分辨率非常适合使用单个CacheManager且没有复杂的缓存分辨率要求的应用程序。

对于使用多个缓存管理器的应用程序,可以设置用于每个操作的cacheManager,如下例所示:

@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1)
public Book findBook(ISBN isbn) {...}
1 指定anotherCacheManager

你还可以完全以类似于替换密钥生成的方式替换CacheResolver。对每个缓存操作都请求解析,让实现根据运行时参数实际解析要使用的缓存。下面的示例展示了如何指定CacheResolver:

@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
1 指定CacheResolver
自 Spring 4.1 起,value属性的缓存注释不再是
强制的,因为该特定信息可以由CacheResolver提供,而不考虑注释的内容。

key类似,cacheManagerkeyGenerator的参数是互斥的,并且指定这两个
的操作会导致异常,因为自定义CacheManagerCacheResolver实现忽略。这可能不是你所期望的。
# 同步缓存

在多线程环境中,某些操作可能会为相同的参数并发调用(通常是在启动时)。默认情况下,缓存抽象不会锁定任何内容,并且相同的值可能会被多次计算,从而破坏了缓存的目的。

对于这些特殊情况,可以使用sync属性来指示底层缓存提供程序在计算值时锁定缓存条目。结果,只有一个线程在忙着计算值,而其他线程则被阻塞,直到在缓存中更新条目。下面的示例展示了如何使用sync属性:

@Cacheable(cacheNames="foos", sync=true) (1)
public Foo executeExpensiveOperation(String id) {...}
1 使用sync属性。
这是一个可选的特性,并且你最喜欢的缓存库可能不支持它。
所有由核心框架提供的CacheManager实现都支持它。有关更多详细信息,请参见缓存提供程序的
文档。
# 条件缓存

有时,一个方法可能不适合始终进行缓存(例如,它可能取决于给定的参数)。缓存注释通过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返回类型。如果一个Optional值是礼物,它将被存储在关联的缓存中。如果不存在Optional值,则null将存储在关联的缓存中。#result总是指业务实体,而不是支持的包装器,因此前面的示例可以重写如下:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)

注意,#result仍然是指Book,而不是Optional<Book>。因为它可能是null,所以我们使用 SPEL 的安全导航操作员

# 可用的缓存 SPEL 评估上下文

每个SpEL表达式都针对一个专用的[context](core.html#expressions-language-ref)计算。除了内置参数外,该框架还提供了专用的与缓存相关的元数据,例如参数名称。下表描述了可用于上下文的项,以便你可以将它们用于键和条件计算:

Name Location 说明 Example
methodName Root object 调用的方法的名称 #root.methodName
method Root object 正在调用的方法 #root.method.name
target Root object 正在调用的目标对象 #root.target
targetClass Root object 被调用的目标的类 #root.targetClass
args Root object 用于调用目标的参数(如数组) #root.args[0]
caches Root object 运行当前方法所针对的缓存的集合 #root.caches[0].name
Argument name Evaluation context 任何方法参数的名称。如果名称不可用
(可能是由于没有调试信息),则参数名称也可以在#a<#arg>下使用,其中#arg代表参数索引(从0开始)。
#iban or #a0 (you can also use #p0 or #p<#arg> notation as an alias).
result Evaluation 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)
在相同的方法上使用@CachePut@Cacheable注释通常不鼓励
,因为它们具有不同的行为。虽然后者导致使用缓存跳过
方法调用,但前者强制执行
命令中的调用,以运行缓存更新。这会导致意想不到的行为,并且,除了
的特定角格(例如注释具有将它们从每个
中排除的条件)以外,应该避免此类声明。还需要注意的是,这样的条件不应该依赖于
上的结果对象(即#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的情况,后者将数据添加到缓存或更新缓存中的数据,因此需要一个结果。

# 8.2.4.@Caching注释

有时,需要指定同一类型的多个注释(例如@CacheEvict@CachePut)——例如,因为不同的缓存之间的条件或密钥表达式是不同的。@Caching让多个嵌套@Cacheable@CachePut@CacheEvict注释在相同的方法上使用。下面的示例使用了两个@CacheEvict注释:

@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。在类上放置此注释不会启动任何缓存操作。

操作级定制总是覆盖@CacheConfig上的定制集。因此,这为每个缓存操作提供了三个级别的自定义:

  • 全局配置,可用于CacheManagerKeyGenerator

  • 在类级别上,使用@CacheConfig

  • 在操作层面。

# 8.2.6.启用缓存注释

重要的是要注意,即使声明缓存注释并不会自动触发它们的动作--就像 Spring 中的许多事情一样,该功能必须以声明方式启用(这意味着,如果你怀疑这是缓存的责任,你可以通过只删除一个配置行而不是代码中的所有注释来禁用它)。

要启用缓存注释,请将注释@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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">

        <cache:annotation-driven/>
</beans>

cache:annotation-driven元素和@EnableCaching注释都允许你指定各种选项,这些选项会影响通过 AOP 将缓存行为添加到应用程序的方式。该配置有意地与[@Transactional](data-access.html#tx-annotation-driven-settings)的配置类似。

处理缓存注释的默认通知模式是proxy,它只允许
通过代理拦截调用。同一类
中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,可以考虑将
转换为aspectj模式与编译时或加载时编织相结合。
有关实现CachingConfigurer所需的
的高级定制(使用 Java 配置)的更多详细信息,请参见javadoc (opens new window)
XML Attribute Annotation Attribute Default 说明
cache-manager N/A (see the CachingConfigurer (opens new window) javadoc) cacheManager 要使用的缓存管理器的名称。默认的CacheResolver
使用此缓存管理器的场景后面初始化(如果未设置cacheManager)。要了解更多
对缓存解析度的细粒度管理,请考虑设置“cache-resolver”
属性。
cache-resolver N/A (see the CachingConfigurer (opens new window) javadoc) A SimpleCacheResolver using the configured cacheManager. Bean 用于解析备份缓存的 CacheResolver 的名称。
此属性不是必需的,只需要指定为
’cache-manager’属性的替代项。
key-generator N/A (see the CachingConfigurer (opens new window) javadoc) SimpleKeyGenerator 要使用的自定义密钥生成器的名称。
error-handler N/A (see the CachingConfigurer (opens new window) javadoc) SimpleCacheErrorHandler 要使用的自定义缓存错误处理程序的名称。默认情况下,在
缓存相关操作期间抛出的任何异常都会在客户端被抛回。
mode mode proxy 默认模式(proxy)处理要通过使用 Spring 的 AOP
框架来代理的注释 bean(遵循代理语义,如前所述,应用于仅通过代理进入的方法调用
)。替代模式(aspectj)使用 Spring 的 AspectJ 缓存方面来编织
受影响的类,修改目标类字节
代码以应用于任何类型的方法调用。AspectJ 编织需要在 Classpath 中启用spring-aspects.jar以及加载时编织(或编译时编织)。(有关如何设置
加载时编织的详细信息,请参见Spring configuration
proxy-target-class proxyTargetClass false 仅适用于代理模式。控制为使用@Cacheable@CacheEvict注释的
类创建的缓存代理类型。如果proxy-target-class属性设置为true,则创建基于类的代理。
如果proxy-target-classfalse,或者如果省略了该属性,则创建基于接口的标准 JDK
代理。(有关不同代理类型的详细检查,请参见代理机制
order order Ordered.LOWEST_PRECEDENCE 定义应用于使用@Cacheable@CacheEvict注释的 bean 的缓存通知的顺序。(有关与
排序 AOP 通知有关的规则的更多信息,请参见建议订购。)
没有指定的排序意味着由 AOP 子系统确定通知的顺序。
<cache:annotation-driven/>仅在与其定义相同的应用程序上下文中的 bean 上查找@Cacheable/@CachePut/@CacheEvict/@Caching。这意味着,如果将
放在<cache:annotation-driven/>中的WebApplicationContext中的DispatcherServlet中的MVC 部门,它将只检查控制器中的 bean,而不是你的服务。
有关更多信息,请参见MVC 部门

方法可见性和缓存注释

当你使用代理时,你应该只将缓存注释应用于具有公共可见性的方法。如果使用这些注释对受保护的、私有的或包可见的方法进行注释,则不会产生错误,但是注释的方法不显示已配置的缓存设置。如果需要对非公共方法进行注释,请考虑使用 AspectJ(请参阅本节的其余部分),因为它会更改字节码本身。

Spring 建议你只使用@Cache*注释来注释具体的类(和具体的
类的方法),而不是注释接口。
你当然可以在接口(或接口
方法)上放置@Cache*注释,但这仅在使用代理模式(mode="proxy")时才有效。如果使用
基于编织的方面(mode="aspectj"),则在
接口级声明上,编织基础设施不会识别缓存设置。
在代理模式(默认)中,只有通过
代理进入的外部方法调用才会被拦截。这意味着,自我调用(实际上,
目标对象中的一个方法调用了目标对象的另一个方法)在运行时不会导致实际的
缓存,即使调用的方法被标记为@Cacheable。在这种情况下,使用aspectj模式考虑
。此外,代理必须完全初始化为
提供预期的行为,因此你不应该在
初始化代码(即@PostConstruct)中依赖此功能。

# 8.2.7.使用自定义注释

自定义注释和 AspectJ

该功能仅适用于基于代理的方法,但可以通过使用 AspectJ 进行一些额外的工作来启用。

spring-aspects模块仅为标准注释定义了一个方面。如果你已经定义了自己的注释,那么还需要为这些注释定义一个方面。检查AnnotationCacheAspect以获取示例。

缓存抽象允许你使用自己的注释来识别触发缓存填充或驱逐的方法。作为一种模板机制,这非常方便,因为它消除了重复缓存注释声明的需要,如果指定了键或条件,或者如果你的代码库中不允许外部导入(org.springframework),这一点特别有用。与stereotype注释的其余部分类似,你可以使用@Cacheable@CachePut@CacheEvict@CacheConfig作为元注释(即可以注释其他注释的注释)。在下面的示例中,我们用自己的自定义注释替换了一个常见的@Cacheable声明:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}

在前面的示例中,我们定义了我们自己的SlowService注释,它本身用@Cacheable注释。现在我们可以替换以下代码:

@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不是 Spring 注释,容器也会在运行时自动获取其声明并理解其含义。注意,正如earlier中提到的,需要启用注释驱动的行为。

# 注解

从版本 4.1 开始, Spring 的缓存抽象完全支持 JCache 标准(JSR-107)的注释:@CacheResult@CachePut@CacheRemove,和@CacheRemoveAll,以及@CacheDefaults@CacheKey伙伴关系。即使不将缓存存储迁移到 JSR-107,也可以使用这些注释。内部实现使用 Spring 的缓存抽象,并提供符合规范的默认CacheResolverKeyGenerator实现。换句话说,如果你已经在使用 Spring 的缓存抽象,那么你可以在不更改缓存存储(或配置)的情况下切换到这些标准注释。

# 8.3.1.功能摘要

对于那些熟悉 Spring 的缓存注释的人,下表描述了 Spring 注释与其 JSR-107 对应注释之间的主要区别:

Spring JSR-107 备注
@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 只支持一个缓存。默认情况下,一个简单的实现基于注释上声明的名称检索要使用的缓存。应该注意的是,如果在注释上没有指定缓存名称,则会自动生成默认值。有关更多信息,请参见@CacheResult#cacheName()的 javadoc。

CacheResolver实例由CacheResolverFactory检索。可以为每个缓存操作定制工厂,如下例所示:

@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class) (1)
public Book findBook(ISBN isbn)
1 为此操作定制工厂。
对于所有引用的类, Spring 尝试定位具有给定类型的 Bean。
如果存在多个匹配,则创建一个新实例,并可以使用常规的
Bean 生命周期回调,例如依赖注入。

键是由javax.cache.annotation.CacheKeyGenerator生成的,其作用与 Spring 的KeyGenerator相同。默认情况下,所有的方法参数都会被考虑在内,除非至少有一个参数是用@CacheKey注释的。这类似于 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 可以管理由带注释的方法引发的异常。这可以防止缓存的更新,但也可以缓存异常作为失败的指示器,而不是再次调用方法。假设如果 ISBN 的结构无效,则抛出InvalidIsbnNotFoundException。这是一个永久性的失败(用这样的参数无法检索到任何一本书)。以下缓存异常,以便使用相同的、无效的 ISBN 的进一步调用直接抛出缓存的异常,而不是再次调用该方法:

@CacheResult(cacheName="books", exceptionCacheName="failures"
            cachedExceptions = InvalidIsbnNotFoundException.class)
public Book findBook(ISBN isbn)

# 8.3.2.启用 JSR-107 支持

除了 Spring 的声明性注释支持外,你不需要做任何特定的操作来启用 JSR-107 支持。如果 Classpath 中同时存在 JSR-107API 和模块,则和XML 元素都会自动启用 JCache 支持。

根据你的用例,选择基本上是你的。你甚至可以混合和
匹配服务,方法是在某些服务上使用 JSR-107API,并在
其他服务上使用 Spring 自己的注释。但是,如果这些服务影响相同的缓存,则应该使用一致的
和相同的密钥生成实现。

# 8.4.声明式基于 XML 的缓存

如果注释不是一种选择(可能是由于无法访问源代码或没有外部代码),则可以使用 XML 进行声明式缓存。因此,你可以在外部指定目标方法和缓存指令,而不是注释用于缓存的方法(类似于声明性事务管理advice)。上一节的示例可以转换为以下示例:

<!-- 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 PointCut 表达式将缓存通知应用到程序中的适当点(更多信息可在Aspect Oriented Programming with Spring中获得)。在前面的示例中,将考虑来自BookService的所有方法,并将缓存通知应用于它们。

声明式 XML 缓存支持所有基于注释的模型,因此在两者之间移动应该很容易。此外,两者都可以在同一个应用程序中使用。基于 XML 的方法不会触及目标代码。然而,它本质上更加冗长。当处理具有针对缓存的重载方法的类时,识别正确的方法确实需要额外的努力,因为method参数不是一个好的鉴别器。在这些情况下,你可以使用 AspectJ 切入点来挑选目标方法并应用适当的缓存功能。然而,通过 XML,应用包或组或接口范围的缓存(同样由于 AspectJ 切入点)和创建模板类定义(就像我们在前面的示例中所做的那样,通过cache:definitions``cache属性定义目标缓存)更容易。

# 8.5.配置缓存存储

缓存抽象提供了几个存储集成选项。要使用它们,你需要声明一个适当的CacheManager(一个控制和管理Cache实例的实体,该实例可用于检索这些实例以进行存储)。

# 8.5.1.基于 jdkConcurrentMap的缓存

基于 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为两个名为ConcurrentMapCachebooks的嵌套实例创建CacheManager。请注意,名称是直接为每个缓存配置的。

由于缓存是由应用程序创建的,因此它与其生命周期绑定在一起,这使得它适合于基本的用例、测试或简单的应用程序。该缓存可以很好地扩展并且非常快,但是它不提供任何管理、持久性功能或驱逐契约。

# 8.5.2.基于 eHcache 的缓存

Ehcache3.x 完全兼容 JSR-107,不需要专门的支持。

EhCache2.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.咖啡因缓存

咖啡因是对芭乐缓存的 Java8 重写,其实现位于org.springframework.cache.caffeine包中,并提供了对咖啡因的几个功能的访问。

下面的示例配置了一个CacheManager,它会按需创建缓存:

<bean id="cacheManager"
        class="org.springframework.cache.caffeine.CaffeineCacheManager"/>

你还可以提供要显式使用的缓存。在这种情况下,只有那些是由经理提供的。下面的示例展示了如何做到这一点:

<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
    <property name="cacheNames">
        <set>
            <value>default</value>
            <value>books</value>
        </set>
    </property>
</bean>

咖啡因CacheManager还支持自定义CaffeineCacheLoader。有关这些参数的更多信息,请参见咖啡因文档 (opens new window)

# 8.5.4.基于 Gemfire 的高速缓存

Gemfire 是一个面向内存、磁盘支持、弹性可伸缩、持续可用、活动的(具有内置的基于模式的订阅通知)、全局复制的数据库,并提供功能齐全的边缘缓存。有关如何使用 Gemfire 作为CacheManager(以及更多)的更多信息,请参见Spring Data GemFire reference documentation (opens new window)

# 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标志,为所有未由配置的缓存管理器处理的定义添加一个无操作缓存。也就是说,在jdkCachegemfireCache(在示例的前面进行了配置)中找不到的每个缓存定义都由不存储任何信息的无运算缓存处理,这会导致每次都调用目标方法。

# 8.6.插入不同的后端缓存

显然,有很多缓存产品可以用作后台商店。对于那些不支持 JSR-107 的,你需要提供CacheManagerCache实现。这听起来可能比实际情况更难,因为在实践中,类往往是简单的adapters (opens new window),它们将缓存抽象框架映射到存储 API 的顶部,就像ehcache类那样。大多数CacheManager类可以使用org.springframework.cache.support包中的类(例如AbstractCacheManager,它负责锅炉板代码,只留下实际的映射来完成)。

# 8.7.我如何设置 TTL/TTI/驱逐策略/XXX 功能?

直接通过你的缓存提供程序。缓存抽象是一个抽象,而不是一个缓存实现。你使用的解决方案可能支持其他解决方案不支持的各种数据策略和不同的拓扑(例如,JDKConcurrentHashMap——在缓存抽象中暴露这一点将是无用的,因为没有支持)。这样的功能应该直接通过后台缓存(在配置时)或通过其本地 API 进行控制。

# 9. 附录

# 9.1.XML 模式

附录的这一部分列出了与集成技术相关的 XML 模式。

# 9.1.1.jee模式

jee元素处理与 Java EE(Java Enterprise 版本)配置有关的问题,例如查找 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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee https://www.springframework.org/schema/jee/spring-jee.xsd">

    <!-- bean definitions here -->

</beans>
# <jee:jndi-lookup/>(简单)

下面的示例展示了如何使用 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/>(复数)

下面的示例展示了如何使用 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/>(简单)

Bean <jee:local-slsb/>元素配置对本地 EJB 无状态会话的引用。

下面的示例展示了如何在没有jee的情况下配置对本地 EJB 无状态会话的引用 Bean:

<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 无状态会话 Bean 的引用:

<jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean"
        business-interface="com.foo.service.RentalService"/>
# <jee:local-slsb/>(复数)

<jee:local-slsb/>元素配置对本地 EJB 无状态会话的引用 Bean。

下面的示例展示了如何配置对本地 EJB 无状态会话 Bean 的引用和一些不带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>

下面的示例展示了如何配置对本地 EJB 无状态会话 Bean 的引用,以及使用jee的许多属性:

<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/>元素配置了对remoteEJB 无状态会话 Bean 的引用。

下面的示例展示了如何在没有jee的情况下配置对远程 EJB 无状态会话 Bean 的引用:

<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 无状态会话 Bean 的引用:

<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 章节题为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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms.xsd">

    <!-- bean definitions here -->

</beans>

# 9.1.3.使用<context:mbean-export/>

这个元素在配置基于注释的 MBean 导出中有详细说明。

# 9.1.4.cache模式

你可以使用cache元素来启用对 Spring 的@CacheEvict@CachePut@Caching注释的支持。它还支持声明式的基于 XML 的缓存。详见启用缓存注释声明式基于 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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">

    <!-- bean definitions here -->

</beans>