# 整合 参考文档的这一部分涵盖了 Spring 框架与许多技术的集成。 ## 1. REST 端点 Spring 框架为调用 REST 端点提供了两种选择: * [`RestTemplate`](#rest-resttemplate):原始的 Spring REST 客户机具有同步的、模板的方法 API。 * [WebClient](web-reactive.html#webflux-client):一种非阻塞、反应性的替代方案,支持同步、异步以及流媒体场景。 | |截至 5.0,`RestTemplate`处于维护模式,只有少量的
更改请求和 bug 被接受。请考虑使用[WebClient](web-reactive.html#webflux-client),它提供了一个更现代的 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` |使用补丁更新资源并返回响应的表示。
注意,JDK`HttpURLConnection`不支持`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 result = restTemplate.getForObject( "https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21"); ``` 下面的示例使用`Map`: ``` Map 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" ``` 可以使用`RestTemplate`的`uriTemplateHandler`属性来定制 URI 的编码方式。或者,你可以准备一个`java.net.URI`并将其传递到一个`RestTemplate`方法,该方法接受`URI`。 有关使用和编码 URI 的更多详细信息,请参见[URI Links](web.html#mvc-uri-building)。 ##### Headers 你可以使用`exchange()`方法来指定请求头,如下例所示: ``` String uriTemplate = "https://example.com/hotels/{hotel}"; URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) .header("MyRequestHeader", "MyValue") .build(); ResponseEntity 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`注册了所有内置的[消息转换器](#rest-message-conversion),这取决于有助于确定存在哪些可选转换库的 Classpath 检查。你还可以将消息转换器设置为显式使用。 #### 1.1.3.消息转换 [WebFlux](web-reactive.html#webflux-codecs) `spring-web`模块包含`HttpMessageConverter`契约,用于通过`InputStream`和`OutputStream`读取和写入 HTTP 请求和响应的主体。在 Spring MVC REST 控制器中)。 MIME 类型的具体实现在框架中提供,并且默认情况下,在客户端用`RestTemplate`注册,在服务器端用注册(参见[配置消息转换器](web.html#mvc-config-message-converters))。 `HttpMessageConverter`的实现方式在以下各节中进行了描述。对于所有转换器,都使用默认的媒体类型,但是你可以通过设置“supportedmediatypes” Bean 属性来覆盖它。下表描述了每种实现方式: | MessageConverter |说明| |----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `StringHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以从 http
请求和响应中读取和写入`String`实例。默认情况下,此转换器支持所有文本媒体类型
,并使用`Content-Type`的`text/plain`进行写入。| | `FormHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以从 HTTP
请求和响应中读写表单数据。默认情况下,此转换器读取和写入“应用程序/X-WWW-表单-URLENCODED”媒体类型。表单数据被读取并写入“multivalueMap”。转换器还可以写入(但不读取)多部分
从`MultiValueMap`中读取的数据。默认情况下,`multipart/form-data`是
支持的。在 Spring Framework5.2 中,对于
写入表单数据,可以支持额外的多部分子类型。有关更多详细信息,请咨询`FormHttpMessageConverter`的 Javadoc。| | `ByteArrayHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以从
HTTP 请求和响应中读写字节数组。默认情况下,此转换器支持所有媒体类型
,并使用`Content-Type`的`application/octet-stream`写。通过设置`supportedMediaTypes`属性并重写`getContentType(byte[])`,可以重写此
。| | `MarshallingHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以通过使用 Spring 的 `marshaller’和`Unmarshaller`包中的抽象来读写 XML。
此转换器需要`Marshaller`和`Unmarshaller`才能使用。你可以通过构造函数或 Bean 属性注入这些
。默认情况下,这个转换器支持“text/xml”和`application/xml`。| | `MappingJackson2HttpMessageConverter` |一个`HttpMessageConverter`实现,它可以通过使用 Jackson 的“ObjectMapper”来读写 JSON。你可以根据需要使用 Jackson 的
提供的注释来定制 JSON 映射。当需要进一步控制时(对于需要为特定类型提供自定义 JSON序列化器/反序列化器的情况),可以通过属性注入自定义。默认情况下,这个
转换器支持`application/json`。| |`MappingJackson2XmlHttpMessageConverter`|一个`HttpMessageConverter`实现,它可以通过使用[Jackson XML](https://github.com/FasterXML/jackson-dataformat-xml)扩展的 `xmlmapper’来读写 XML。你可以根据需要通过使用 JAXB
或 Jackson 提供的注释来定制 XML 映射。当需要进一步控制时(对于需要为特定类型提供自定义 XML序列化器/反序列化器的情况),可以通过属性注入自定义。默认情况下,这个
转换器支持`application/xml`。| | `SourceHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以从 HTTP 请求和响应中读写 `javax.xml.transform.source’。只支持`DOMSource`、`saxsource’和`StreamSource`。默认情况下,这个转换器支持“text/xml”和`application/xml`。| | `BufferedImageHttpMessageConverter` |一个`HttpMessageConverter`实现,可以从 HTTP 请求和响应中读写 `java.awt.image.BufferidImage’。这个转换器读取
并写入 Java I/O API 支持的媒体类型。| #### 1.1.4.JacksonJSON 视图 你可以指定[JacksonJSON 视图](https://www.baeldung.com/jackson-json-view-annotation)来序列化对象属性的一个子集,如下例所示: ``` MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); value.setSerializationView(User.WithoutPasswordView.class); RequestEntity requestEntity = RequestEntity.post(new URI("https://example.com/user")).body(value); ResponseEntity response = template.exchange(requestEntity, String.class); ``` ##### 多部分 要发送多部分数据,你需要提供一个`MultiValueMap`,其值对于部分内容可以是`Object`,对于文件部分可以是`Resource`,对于带有标题的部分内容可以是`HttpEntity`。例如: ``` MultiValueMap 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 parts = ...; template.postForObject("https://example.com/upload", parts, Void.class); ``` 如果`MultiValueMap`至少包含一个非 ` 字符串’值,则`Content-Type`由`FormHttpMessageConverter`设置为`multipart/form-data`。如果`MultiValueMap`具有 `string` 值,则`Content-Type`默认为`application/x-www-form-urlencoded`。如果有必要,`Content-Type`也可以显式地设置。 ### 1.2.使用`AsyncRestTemplate`(不推荐) `AsyncRestTemplate`已被弃用。对于可能考虑使用“AsyncrestTemplate”的所有用例,请使用[WebClient](web-reactive.html#webflux-client)。 ## 2. 远程和 Web 服务 Spring 通过各种技术为远程控制提供支持。远程支持简化了支持远程的服务的开发,这些服务是通过 Java 接口和对象作为输入和输出来实现的。目前, Spring 支持以下远程处理技术: * [Java Web 服务](#remoting-web-services): Spring 通过 JAX-WS 为 Web 服务提供远程支持。 * [AMQP](#remoting-amqp):单独的 Spring AMQP 项目支持通过 AMQP 作为底层协议进行远程处理。 | |从 Spring Framework5.3 开始,出于安全原因和更广泛的行业支持,现在不赞成对几种远程技术的支持
。支持基础设施将从 Spring 框架中删除
,用于其下一个主要版本。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 以下远程处理技术现已弃用,不会被替换: * [RMI](#remoting-rmi):通过使用`RmiProxyFactoryBean`和 `RMIServiceExporter’, Spring 既支持传统的 RMI(带有`java.rmi.Remote`接口和`java.rmi.RemoteException`接口),也支持通过 RMI 调用程序(带有任何 Java 接口)进行透明的远程处理。 * [Spring HTTP Invoker (Deprecated)](#remoting-httpinvoker): Spring 提供了一种特殊的远程策略,该策略允许通过 HTTP 进行 Java 序列化,支持任何 Java 接口(就像 RMI 调用程序所做的那样)。对应的支持类是`HttpInvokerProxyFactoryBean`和`HttpInvokerServiceExporter`。 * [Hessian](#remoting-caucho-protocols-hessian):通过使用 Spring 的`HessianProxyFactoryBean`和 `HessianServiceExporter’,你可以通过 Caucho 提供的基于 HTTP 的轻量级二进制协议透明地公开你的服务。 * [JMS(已弃用)](#remoting-jms):通过 JMS 作为基础协议的远程处理,通过 ` Spring-JMS` 模块中的 `jmsinvokerServiceExporter’和`JmsInvokerProxyFactoryBean`类得到支持。 在讨论 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 getAccounts(String name); } ``` ``` // the implementation doing nothing at the moment public class AccountServiceImpl implements AccountService { public void insertAccount(Account acc) { // do something... } public List getAccounts(String name) { // do something... } } ``` 本节首先通过使用 RMI 将服务公开给远程客户机,并稍微讨论一下使用 RMI 的缺点。然后继续以使用 Hessian 作为协议的示例进行说明。 ### 2.1.AMQP Spring AMQP 项目支持通过 AMQP 作为底层协议的远程处理。欲了解更多详情,请访问 Spring AMQP 参考文献的[Spring Remoting](https://docs.spring.io/spring-amqp/docs/current/reference/html/#remoting)部分。 | |远程接口未实现自动检测。

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

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

这是在配置便利和意外
暴露内部方法的风险之间的权衡。始终指定一个服务接口并不需要太多的
工作,并且对于控制特定方法的公开,这会使你处于安全的一边。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 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](https://projects.spring.io/spring-ws),这是一种契约优先、文档驱动的 Web 服务的解决方案——强烈推荐用于构建现代的、面向未来的 Web 服务。 #### 2.3.1.使用 JAX-WS 公开基于 Servlet 的 Web 服务 Spring 为 JAX-WS Servlet 端点实现提供了一个方便的基类:“SpringBeanAutoWiringSupport”。为了公开我们的`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 本身并作为 bean 进行管理。它们在 JAX-WS 引擎中注册,但它们的生命周期取决于 Spring 应用程序上下文。这意味着你可以将 Spring 功能(例如显式依赖注入)应用到端点实例。通过`@Autowired`的注解驱动注入也可以工作。下面的示例展示了如何定义这些 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 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-managed bean,类似于[上一节](#remoting-web-services-jaxws-export-standalone)中讨论的独立模式——但这次是在 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/](https://jax-ws-commons.java.net/spring/)。 #### 2.3.4.使用 JAX-WS 访问 Web 服务 Spring 提供了两个工厂 bean 来创建 JAX-WS Web 服务代理,即 localJaxWsServiceFactoryBean` 和。前者只能返回一个 JAX-WS 服务类供我们使用。后者是成熟的版本,可以返回实现我们的业务服务接口的代理。在下面的示例中,我们使用`JaxWsPortProxyFactoryBean`为“AccountService”端点创建代理(再次): ``` (1) ``` |**1**|其中`serviceInterface`是客户机使用的业务接口。| |-----|------------------------------------------------------------------------| `wsdlDocumentUrl`是 WSDL 文件的 URL。 Spring 在启动时需要这个来创建 JAX-WS 服务。`namespaceUri`对应于.wsdl 文件中的`targetNamespace`。`serviceName`对应于.wsdl 文件中的服务名称。`portName`对应于.wsdl 文件中的端口号。 访问 Web 服务很容易,因为我们有一个 Bean 工厂将其公开为一个名为`AccountService`的接口。下面的示例展示了我们如何在 Spring 中将其连接起来: ``` ... ``` 从客户机代码中,我们可以像访问普通类一样访问 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 容器中设置我们的服务。下面的示例展示了如何做到这一点: ``` ``` 接下来,我们必须使用`RmiServiceExporter`公开我们的服务。下面的示例展示了如何做到这一点: ``` ``` 在前面的示例中,我们重写了 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 容器,以包含以下简单的对象和服务连接配置位: ``` ``` 这就是我们在客户端上支持远程帐户服务所需要做的全部工作。 Spring 透明地创建调用程序,并通过“rmiserviceexporter”远程启用帐户服务。在客户端,我们使用`RmiProxyFactoryBean`将其连接进来。 ### 2.5.使用 Hessian 通过 HTTP 远程调用服务(已弃用) | |截至 Spring 框架 5.3,Hessian 支持已被弃用,并且不会被替换。| |---|-----------------------------------------------------------------------------------| Hessian 提供了一种基于二进制 HTTP 的远程处理协议。它是由 Caucho 开发的,你可以在[https://www.caucho.com/](https://www.caucho.com/)上找到有关黑森本身的更多信息。 #### 2.5.1.黑森 Hessian 通过 HTTP 进行通信,并通过使用自定义 Servlet 进行通信。通过使用 Spring 的“DispatcherServlet”原则(参见[webmvc.html](webmvc.html#mvc-servlet)),我们可以连接这样的 Servlet 来公开你的服务。首先,我们必须在我们的应用程序中创建一个新的 Servlet,如以下`web.xml`的节选所示: ``` remoting org.springframework.web.servlet.DispatcherServlet 1 remoting /remoting/* ``` 如果你熟悉 Spring 的`DispatcherServlet`原则,那么你可能知道,现在你必须在`WEB-INF`目录中创建一个名为 `remoting- Servlet.xml` 的 Spring 容器配置资源(以你的 Servlet 的名称命名)。应用程序上下文将在下一节中使用。 或者,考虑使用 Spring 的更简单的`HttpRequestHandlerServlet`。这样,你就可以在根应用程序上下文(默认情况下,在`WEB-INF/applicationContext.xml`中)中嵌入远程导出定义,并使用单独的 Servlet 定义指向特定的导出 bean。在这种情况下,每个 Servlet 名称需要匹配其目标输出器的 Bean 名称。 #### 2.5.2.使用`HessianServiceExporter`暴露 bean 在新创建的名为`remoting-servlet.xml`的应用程序上下文中,我们创建一个 `HessianServiceExporter’来导出我们的服务,如下例所示: ``` ``` 现在,我们已经准备好在客户端的服务链接。没有指定显式的处理程序映射(以将请求 URL 映射到服务上),因此我们使用`BeanNameUrlHandlerMapping`。因此,该服务在包含`DispatcherServlet`实例映射(如前面定义的)中的 Bean 名称所指示的 URL 处导出:`[https://host:8080/remoting/accountservice](https://HOST:8080/remoting/AccountService)`。 或者,你可以在根应用程序上下文中创建`HessianServiceExporter`(例如,在`WEB-INF/applicationContext.xml`中),如下例所示: ``` ``` 在后一种情况下,你应该在`web.xml`中为这个输出器定义一个相应的 Servlet,其最终结果是相同的:输出器被映射到位于 `/remoting/accountService` 处的请求路径。请注意, Servlet 名称需要与目标输出器的 Bean 名称匹配。下面的示例展示了如何做到这一点: ``` accountExporter org.springframework.web.context.support.HttpRequestHandlerServlet accountExporter /remoting/AccountService ``` #### 2.5.3.在客户端的服务中链接 通过使用`HessianProxyFactoryBean`,我们可以在客户端的服务中进行链接。同样的原则也适用于 RMI 的例子。我们创建一个单独的 Bean 工厂或应用程序上下文,并提到以下 bean,其中`SimpleObject`是通过使用`AccountService`来管理帐户的,如下例所示: ``` ``` #### 2.5.4.将 HTTP 基本身份验证应用于通过 Hessian 公开的服务 #### Hessian 的优点之一是我们可以轻松地应用 HTTP 基本身份验证,因为这两个协议都是基于 HTTP 的。例如,可以通过使用`web.xml`安全特性来应用正常的 HTTP 服务器安全机制。通常,在此不需要使用每个用户的安全凭据。相反,你可以使用在`HessianProxyFactoryBean`级别定义的共享凭据(类似于 JDBC`DataSource`),如下例所示: ``` ``` 在前面的示例中,我们明确地提到了`BeanNameUrlHandlerMapping`并设置了一个拦截器,以便只让管理员和操作员调用在此应用程序上下文中提到的 bean。 | |前面的示例并未展示一种灵活的安全基础架构。对于
关于安全性的更多选项,请查看 Spring 安全性项目
at[https://projects.spring.io/spring-security/](https://projects.spring.io/spring-security/)。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 2.6. Spring HTTP Invoker(不推荐) | |在 Spring Framework5.3 中,HTTP Invoker 支持已被弃用,不会被替换。| |---|----------------------------------------------------------------------------------------| Spring HTTP 调用程序与 Hessian 相反,都是轻量级协议,它们使用自己的 Slim 序列化机制,并使用标准的 Java 序列化机制通过 HTTP 公开服务。如果你的参数和返回类型是复杂的类型,无法通过使用 Hessian 使用的序列化机制进行序列化,那么这将具有巨大的优势(在选择远程技术时,请参阅下一节以了解更多的考虑因素)。 在这种情况下, Spring 使用 JDK 或 Apache`HttpComponents`提供的标准工具来执行 HTTP 调用。如果你需要更高级、更易用的功能,请使用后者。有关更多信息,请参见[hc.apache.org/httpcomponents-client-ga/](https://hc.apache.org/httpcomponents-client-ga/)。 | |注意由不安全的 Java 反序列化引起的漏洞:
在反序列化步骤期间,被操纵的输入流可能导致服务器上执行不需要的代码
。因此,不要将 HTTP Invoker
端点公开给不受信任的客户端。相反,只在你自己的服务之间公开它们。
总的来说,我们强烈建议使用任何其他消息格式(例如 JSON),

如果你担心 Java 序列化带来的安全漏洞,
请考虑核心 JVM 级别的通用序列化过滤机制,
最初是为 JDK9 开发的,但后来移植到了 JDK8,同时是 7 号和 6 号。见[https://blogs.oracle.com/java-platform-group/entry/incoming\_filter\_serialization\_data\_a](https://blogs.oracle.com/java-platform-group/entry/incoming_filter_serialization_data_a)和[https://openjdk.java.net/jeps/290](https://openjdk.java.net/jeps/290)。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 2.6.1.公开服务对象 为服务对象设置 HTTP 调用程序基础结构与使用 Hessian 进行相同设置的方式非常相似。由于 Hessian 支持提供了“HessianServiceExporter”, Spring 的 Httpinvoker 支持提供了“org.SpringFramework.Remoting.Httpinvoker.HttpinvokerServiceExporter”。 要在 Spring Web MVC`DispatcherServlet’中公开`AccountService`(前面提到过),需要在 Dispatcher 的应用程序上下文中设置以下配置,如下例所示: ``` ``` 这样的导出定义通过`DispatcherServlet`实例的标准映射工具公开,如[关于黑森的章节](#remoting-caucho-protocols)中所解释的那样。 或者,你可以在根应用程序上下文中创建`HttpInvokerServiceExporter`(例如,在`'WEB-INF/applicationContext.xml'`中),如下例所示: ``` ``` 此外,可以在`web.xml`中为该输出器定义相应的 Servlet,其 Servlet 名称与目标输出器的 Bean 名称匹配,如下例所示: ``` accountExporter org.springframework.web.context.support.HttpRequestHandlerServlet accountExporter /remoting/AccountService ``` #### 2.6.2.在客户端的服务中链接 同样,从客户机在服务中进行链接与使用 Hessian 时的方式非常相似。通过使用代理, Spring 可以将对 HTTP POST 请求的调用转换为指向导出服务的 URL。下面的示例展示了如何配置这种安排: ``` ``` 如前所述,你可以选择要使用的 HTTP 客户机。默认情况下,“HttpinVokerProxy”使用 JDK 的 HTTP 功能,但你也可以通过设置`httpInvokerRequestExecutor`属性来使用 Apache“HttpComponents”客户端。下面的示例展示了如何做到这一点: ``` ``` ### 2.7.JMS(已弃用) | |从 Spring Framework5.3 开始,JMS 远程支持已被弃用,不会被替换。| |---|----------------------------------------------------------------------------------------| 你还可以通过使用 JMS 作为底层通信协议来透明地公开服务。 Spring 框架中的 JMS 远程支持非常基本。它在`same thread`上发送和接收,并在相同的非事务性`Session`上发送和接收。因此,吞吐量是依赖于实现的。请注意,这些单线程和非事务约束仅适用于 Spring 的 JMS 远程支持。有关 Spring 对基于 JMS 的消息传递的丰富支持的信息,请参见[JMS(Java 消息服务)](#jms)。 服务器端和客户端都使用以下接口: ``` package com.foo; public interface CheckingAccountService { public void cancelAccount(Long accountId); } ``` 在服务器端使用了前面接口的以下简单实现: ``` package com.foo; public class SimpleCheckingAccountService implements CheckingAccountService { public void cancelAccount(Long accountId) { System.out.println("Cancelling account [" + accountId + "]"); } } ``` 以下配置文件包含在客户机和服务器上共享的 JMS-Infrastructure bean: ``` ``` #### 2.7.1.服务器端配置 在服务器上,你需要公开使用“jmsinVokerServiceExporter”的服务对象,如下例所示: ``` ``` ``` 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 将调用转发到服务器端对象): ``` ``` ``` 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 接口,并提供一个实现`SessionBean`和`MyComponent`业务方法接口的实现类。现在,要将 Web 层控制器连接到 EJB 实现,我们需要做的唯一 Java 编码是在控制器上公开一个类型`MyComponent`的 setter 方法。这将引用保存为控制器中的实例变量。下面的示例展示了如何做到这一点: ``` private MyComponent myComponent; public void setMyComponent(MyComponent myComponent) { this.myComponent = myComponent; } ``` 随后,我们可以在控制器中的任何业务方法中使用此实例变量。现在,假设我们从 Spring 容器中获得控制器对象,我们可以(在相同的上下文中)配置`LocalStatelessSessionProxyFactoryBean`实例,它是 EJB 代理对象。我们配置代理,并使用以下配置条目设置控制器的“mycomponent”属性: ``` ``` 由于 Spring AOP 框架,大量的工作在幕后进行,尽管你并不是被迫使用 AOP 概念来享受结果。 Bean“MyComponent”定义为 EJB 创建了一个代理,该代理实现了业务方法接口。EJB 本地主页是在启动时缓存的,因此只有一个 JNDI 查找。每次调用 EJB 时,代理都调用本地 EJB 上的`classname`方法,并在 EJB 上调用相应的业务方法。 `myController` Bean 定义将控制器类的`myComponent`属性设置为 EJB 代理。 或者(最好是在许多这样的代理定义的情况下),考虑在 Spring 的“jee”命名空间中使用``配置元素。下面的示例展示了如何做到这一点: ``` ``` 这种 EJB 访问机制极大地简化了应用程序代码。Web 层代码(或其他 EJB 客户机代码)不依赖于 EJB 的使用。要用 POJO 或模拟对象或其他测试存根替换此 EJB 引用,我们可以在不更改一行 Java 代码的情况下更改`myComponent` Bean 定义。此外,我们不需要编写一行 JNDI 查找或其他 EJB 管道代码作为应用程序的一部分。 实际应用程序中的基准测试和经验表明,这种方法(涉及目标 EJB 的反射调用)的性能开销很小,并且在典型使用中是无法检测到的。请记住,我们无论如何都不想对 EJB 进行细粒度的调用,因为在应用程序服务器中存在与 EJB 基础设施相关的成本。 有一个关于 JNDI 查找的警告。在 Bean 容器中,这个类通常最好作为单例使用(没有理由将其作为原型)。但是,如果 Bean 容器预先实例化了单例(就像各种 XML`ApplicationContext’变体一样),那么如果 Bean 容器是在 EJB 容器加载目标 EJB 之前加载的,那么你可能会遇到问题。这是因为 JNDI 查找是在该类的`init()`方法中执行的,然后进行缓存,但是 EJB 还没有被绑定到目标位置。解决方案是不预先实例化这个工厂对象,而是让它在第一次使用时就被创建。在 XML 容器中,你可以通过使用`lazy-init`属性来控制这一点。 虽然大多数 Spring 用户不感兴趣,但是那些使用 EJB 进行编程工作的用户可能希望查看`LocalSlsbInvokerInterceptor`。 #### 3.1.3.访问远程 SLSB 访问远程 EJB 本质上与访问本地 EJB 相同,只是使用了“SimpleRemoteStatelessionProxyFactoryBean”或配置元素。当然,不管有没有 Spring,远程调用语义都适用:对另一台计算机中另一 VM 中的对象上的方法的调用,有时确实必须在使用场景和故障处理方面进行不同的处理。 Spring 的 EJB 客户机支持比非 Spring 方法增加了一个优势。通常情况下,EJB 客户机代码在本地或远程调用 EJB 之间容易地来回切换是有问题的。这是因为远程接口方法必须声明它们抛出`RemoteException`,而客户端代码必须处理这个问题,而本地接口方法则不需要。为本地 EJB 编写的客户端代码需要迁移到远程 EJB,通常需要对其进行修改,以添加对远程异常的处理,而为远程 EJB 编写的、需要转移到本地 EJB 的客户机代码可以保持不变,但可以对远程异常进行大量不必要的处理,或者进行修改以删除该代码。使用 Spring 远程 EJB 代理,你可以不声明在你的业务方法接口和实现 EJB 代码中抛出的任何,而是具有相同的远程接口(除了它确实抛出),并依赖代理来动态地对待这两个接口,就像它们是相同的一样。即,客户端代码不必处理选中的`RemoteException`类。在 EJB 调用期间抛出的任何实际`RemoteException`都将被重新抛出为未选中的`RemoteAccessException`类,它是`RuntimeException`的子类。然后,你可以在本地 EJB 或远程 EJB(甚至是普通的 Java 对象)实现之间随意切换目标服务,而不需要客户机代码知道或关心。当然,这是可选的:没有什么可以阻止你在业务接口中声明`RemoteException`。 #### 3.1.4.访问 EJB2.x slsbs 与 EJB3slsbs 通过 Spring 访问 EJB2.x 会话 bean 和 EJB3 会话 bean 在很大程度上是透明的。 Spring 的 EJB 访问器,包括``和’设施,在运行时透明地适应实际组件。如果找到了 home 接口(EJB2.x 样式),他们将处理该接口;如果没有 home 接口,他们将执行直接的组件调用(EJB3 样式)。 注意:对于 EJB3 会话 bean,你也可以有效地使用`JndiObjectFactoryBean`/`,因为完全可用的组件引用在那里公开用于普通的 JNDI 查找。定义显式``或``查找提供了一致和更显式的 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 的`PlatformTransactionManager`for 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`的代码只需要实现回调接口,从而为它们提供一个明确定义的高级契约。当`MessageCreator`调用代码在`JmsTemplate`中提供了`Session`时,回调接口将创建一条消息。为了允许更复杂地使用 JMS API,`SessionCallback`提供了 JMS 会话,而`ProducerCallback`公开了一个`Session`和 `MessageProducer’对。 JMS API 公开了两种类型的发送方法,一种是将交付模式、优先级和实时作为服务质量参数,另一种是不接受 QoS 参数并使用默认值。由于`JmsTemplate`有许多发送方法,所以将 QoS 参数设置为 Bean 属性已公开,以避免发送方法数量的重复。类似地,同步接收调用的超时值是通过使用`setReceiveTimeout`属性设置的。 一些 JMS 提供程序允许通过`ConnectionFactory`的配置在管理上设置默认的 QoS 值。这样做的结果是,调用 `MessageProducer’实例的`send`方法(`Send(DestinateDestination,MessageMessage Message)’)所使用的 QoS 默认值与 JMS 规范中指定的值不同。因此,为了提供一致的 QoS 值管理,必须通过将布尔属性 `isexplicitqosenabled’设置为,明确地使能够使用其自己的 QoS 值。 为了方便起见,`JmsTemplate`还公开了一个基本的请求-答复操作,该操作允许在作为操作的一部分而创建的临时队列上发送消息并等待答复。 | |一旦配置好,`JmsTemplate`类的实例是线程安全的。这是
重要的,因为这意味着你可以配置`JmsTemplate`的单个实例,然后安全地将此共享引用注入多个协作者。要使
清晰,`JmsTemplate`是有状态的,因为它保持了对 `connectionFactory’的引用,但该状态不是会话状态。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在 Spring Framework4.1 中,`JmsMessagingTemplate`构建在`JmsTemplate`之上,并提供了与消息抽象的集成——即 `org.springframework.messaging.message`。这使你能够以通用的方式创建要发送的消息。 #### 4.1.2.连接 `JmsTemplate`需要引用`ConnectionFactory`。`ConnectionFactory`是 JMS 规范的一部分,是使用 JMS 的入口点。客户机应用程序将其用作工厂,以创建与 JMS 提供者的连接,并封装各种配置参数,其中许多是特定于供应商的,例如 SSL 配置选项。 当在 EJB 中使用 JMS 时,供应商提供 JMS 接口的实现,以便他们可以参与声明式事务管理并执行连接和会话的池。为了使用此实现,Java EE 容器通常要求你在 EJB 或 Servlet 部署描述符中将 JMS 连接工厂声明为`resource-ref`。为了确保在 EJB 中的“JMStemplate”中使用这些特性,客户端应用程序应该确保它引用`ConnectionFactory`的托管实现。 ##### 缓存消息传递资源 标准 API 涉及创建许多中间对象。要发送消息,需要执行以下“API”遍历: ``` ConnectionFactory->Connection->Session->MessageProducer->send ``` 在`ConnectionFactory`和`Send`操作之间,创建并销毁了三个中间对象。为了优化资源使用并提高性能, Spring 提供了`ConnectionFactory`的两种实现方式。 ##### 使用`SingleConnectionFactory` Spring 提供了`ConnectionFactory`接口 `SingleConnectionFactory’的实现,该实现在所有 `createConnection()’调用上返回相同的`Connection`,并忽略对`close()`的调用。这对于测试和独立环境非常有用,因此同一个连接可以用于多个“JMStemplate”调用,这些调用可以跨越任意数量的事务。`SingleConnectionFactory`引用了通常来自 JNDI 的标准`ConnectionFactory`。 ##### 使用`CachingConnectionFactory` `CachingConnectionFactory`扩展了`SingleConnectionFactory`的功能,并添加了`Session`、`MessageProducer`和`MessageConsumer`实例的缓存。初始缓存大小设置为`1`。你可以使用`sessionCacheSize`属性来增加缓存的会话的数量。请注意,实际缓存的会话的数量要多于这个数量,因为会话是基于它们的确认模式进行缓存的,因此当`sessionCacheSize`设置为 1 时,最多可以有 4 个缓存的会话实例(每个确认模式一个)。`MessageProducer`和`MessageConsumer`实例在其所属会话中进行缓存,并且在缓存时还考虑到生产者和消费者的独特属性。消息生成器是根据它们的目的地进行缓存的。MessageConsumer 是基于一个由 Destination、Selector、noLocal Delivery 标志和持久订阅名称(如果创建持久消费者的话)组成的键来缓存的。 #### 4.1.3.目的地管理 Destinations 作为`ConnectionFactory`实例,是可以在 JNDI 中存储和检索的 JMS 管理的对象。在配置 Spring 应用程序上下文时,可以使用 JNDI工厂类或对对象对 JMS 目的地的引用执行依赖项注入。但是,如果应用程序中有大量的目的地,或者如果有 JMS 提供程序独有的高级目的地管理功能,那么这种策略通常会很麻烦。这种高级目的地管理的示例包括创建动态目的地或支持目的地的分级命名空间。`JmsTemplate`将目标名称的解析委托给实现“destinationResolver”接口的 JMS Destination 对象。`DynamicDestinationResolver`是`JmsTemplate`使用的默认实现,并适应解析动态目的地。还提供了一个“JNDestInationResolver”,作为 JNDI 中包含的目的地的服务定位器,并可选地退回到“DynamicDestinationResolver”中包含的行为。 通常情况下,JMS 应用程序中使用的目标仅在运行时是已知的,因此,在部署应用程序时不能以管理方式创建目标。这通常是因为在交互的系统组件之间存在共享的应用程序逻辑,这些组件根据众所周知的命名约定在运行时创建目标。尽管动态目的地的创建不是 JMS 规范的一部分,但大多数供应商都提供了这种功能。动态目的地是用用户定义的名称创建的,这将它们与临时目的地区分开来,并且通常不会在 JNDI 中注册。用于创建动态目的地的 API 因提供者而异,因为与目的地关联的属性是特定于供应商的。然而,供应商有时会做出一个简单的实现选择,那就是忽略 JMS 规范中的警告,并使用方法`TopicSession`createtopic(字符串 topicname)` 或`QueueSession``createQueue(String queueName)`方法创建具有默认目标属性的新目标。根据供应商的实现,`DynamicDestinationResolver`还可以创建一个物理目的地,而不是只解决一个。 布尔属性`pubSubDomain`用于配置`JmsTemplate`,以了解正在使用的 JMS 域。默认情况下,此属性的值为 false,表示要使用点对点域`Queues`。此属性(由`JmsTemplate`使用)通过`DestinationResolver`接口的实现来确定动态目标解析的行为。 你还可以通过属性`defaultDestination`配置带有默认目标的`JmsTemplate`。默认的目标是不引用特定目标的发送和接收操作。 #### 4.1.4.消息监听器容器 在 EJB 世界中,JMS 消息最常见的用途之一是驱动消息驱动 Bean。 Spring 提供了一种解决方案,以不将用户绑定到 EJB 容器的方式创建消息驱动的 POJO。(关于 Spring 的 MDP 支持的详细介绍,请参见[异步接收:消息驱动的 POJO](#jms-receiving-async)。)自 Spring Framework4.1 以来,端点方法可以使用`@JmsListener`进行注释——有关更多详细信息,请参见[注释驱动的监听器端点](#jms-annotated)。 消息侦听器容器用于接收来自 JMS 消息队列的消息,并驱动注入其中的`MessageListener`。侦听器容器负责消息接收的所有线程处理,并将消息发送到侦听器中进行处理。消息侦听器容器是 MDP 和消息提供程序之间的中介,负责注册以接收消息、参与事务、资源获取和释放、异常转换等。这使你能够编写与接收消息(并可能对消息做出响应)相关的(可能复杂的)业务逻辑,并将 JMS 基础设施关注的样板委托给框架。 有两个打包了 Spring 的标准 JMS 消息侦听器容器,每个容器都有其专门的功能集。 * [SimpleMessageListenerContainer’](#jms-mdp-simple) * [“DefaultMessagelistenerContainer”](#jms-mdp-default) ##### 使用`SimpleMessageListenerContainer` 此消息侦听器容器是两种标准类型中较简单的一种。它在启动时创建固定数量的 JMS 会话和使用者,通过使用标准的 JMS`MessageConsumer.setMessageListener()`方法注册侦听器,并将其留给 JMS 提供者来执行侦听器回调。这种变体不允许对运行时需求进行动态调整,也不允许参与外部管理的事务。在兼容性方面,它非常接近独立的 JMS 规范的精神,但通常与 Java EE 的 JMS 限制不兼容。 | |虽然`SimpleMessageListenerContainer`不允许参与外部
托管事务,但它确实支持本机 JMS 事务。要启用此功能,
你可以将`sessionTransacted`标志切换到`true`,或者在 XML 命名空间中,将 `concount’属性设置为`transacted`。从侦听器抛出的异常将导致
回滚,并重新传递消息。或者,考虑使用“Client_Account”模式,该模式在出现异常的情况下也提供重新交付,但是
不使用已处理的`Session`实例,因此不包括事务协议中的任何其他“会话”操作(例如发送响应消息)。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |默认的`AUTO_ACKNOWLEDGE`模式不能提供适当的可靠性保证,当监听器执行失败时,
消息可能会丢失(因为提供者在监听器调用后自动
确认每条消息,没有异常情况要传播到
提供程序)或当侦听器容器关闭时(你可以通过设置
`acceptMessagesWhileStopping`标志来配置这一点)。确保在
可靠性需要的情况下使用事务会话(例如,用于可靠的队列处理和持久的主题订阅)。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 使用`DefaultMessageListenerContainer` 此消息侦听器容器在大多数情况下都被使用。与“SimpleMessageListenerContainer”相反,这种容器变体允许对运行时需求进行动态调整,并且能够参与外部管理的事务。当使用“JTATRANSACTIONMANAGER”配置时,每个接收到的消息都会在 XA 事务中注册。因此,处理可以利用 XA 事务语义。这个侦听器容器在对 JMS 提供者的低要求、高级功能(例如参与外部管理的事务)以及与 Java EE 环境的兼容性之间取得了良好的平衡。 你可以自定义容器的缓存级别。请注意,当不启用缓存时,将为每个消息接收创建一个新的连接和一个新的会话。将此与具有高负载的非持久性订阅结合在一起可能会导致消息丢失。在这种情况下,请确保使用适当的缓存级别。 当代理发生故障时,这个容器还具有可恢复的功能。默认情况下,一个简单的`BackOff`实现每五秒重试一次。你可以为更细粒度的恢复选项指定一个自定义`BackOff`实现。有关示例,请参见[“指数后退”](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 事务(通过配置你的 `defaultMessageListenerContainer` 和`JtaTransactionManager`)来覆盖
接收 JMS 消息以及在你的
消息侦听器中执行业务逻辑(包括数据库操作等)。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |默认的`AUTO_ACKNOWLEDGE`模式不能提供适当的可靠性保证,当监听器执行失败时,
消息可能会丢失(因为提供者在监听器调用后自动
确认每条消息,没有异常情况要传播到
提供程序)或当侦听器容器关闭时(你可以通过设置
`acceptMessagesWhileStopping`标志来配置此项)。确保在
可靠性需要的情况下使用事务会话(例如,用于可靠的队列处理和持久的主题订阅)。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 4.1.5.事务管理 Spring 提供了一个`JmsTransactionManager`,它为单个 JMS`ConnectionFactory’管理事务。这使得 JMS 应用程序能够利用 Spring 的托管事务特性,如[数据访问章节的事务管理部分](data-access.html#transaction)中所述。`JmsTransactionManager`执行本地资源事务,将指定的`ConnectionFactory`中的一个 JMS 连接/会话对绑定到线程。 在 Java EE 环境中,`ConnectionFactory`池连接和会话实例,因此这些资源可以在事务之间有效地重用。在独立环境中,使用 Spring 的`SingleConnectionFactory`会导致共享的 JMS`Connection`,每个事务都有自己独立的`Session`。或者,考虑使用特定于提供程序的池适配器,例如 ActiveMQ 的`PooledConnectionFactory`类。 你还可以使用`JmsTemplate`和`JtaTransactionManager`以及支持 XA 的 JMS`ConnectionFactory’来执行分布式事务。请注意,这需要使用 JTA 事务管理器以及正确配置 XA 的 ConnectionFactory。(检查你的 Java EE 服务器或 JMS 提供者的文档。 当使用 JMS API 从`Connection`创建`Session`时,跨托管和非托管事务环境重用代码可能会引起混淆。这是因为 JMS API 只有一个工厂方法来创建`Session`,并且它需要事务和确认模式的值。在托管环境中,设置这些值是环境的事务基础设施的责任,因此供应商对 JMS 连接的包装器忽略了这些值。在非托管环境中使用`JmsTemplate`时,可以通过使用属性`sessionTransacted`和`sessionAcknowledgeMode`来指定这些值。当你使用带有`JmsTemplate`的 `Platform TransactionManager’时,模板总是被赋予一个事务性 JMS`Session`。 ### 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`并指定了一个默认的目的地,那么“发送”将向该目的地发送一条消息。 #### 4.2.1.使用消息转换器 为了促进域模型对象的发送,`JmsTemplate`有各种发送方法,这些方法将 Java 对象作为消息数据内容的参数。jmstemplate 中的重载方法`convertAndSend()`和`receiveAndConvert()`方法将转换过程委托给`MessageConverter`接口的实例。这个接口定义了一个简单的契约,用于在 Java 对象和 JMS 消息之间进行转换。默认实现支持`String`和`TextMessage`、`byte[]`和`BytesMessage`之间的转换,以及`java.util.Map`和`MapMessage`之间的转换。通过使用转换器,你和你的应用程序代码可以专注于通过 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.使用`SessionCallback`和`ProducerCallback` 虽然发送操作涵盖了许多常见的使用场景,但有时你可能希望在 JMS`Session`或`MessageProducer`上执行多个操作。“sessionCallback”和`ProducerCallback`分别公开 JMS`Session`和`Session`/“MessageProducer”对。在`JmsTemplate`上的`execute()`方法运行这些回调方法。 ### 4.3.接收消息 这描述了如何使用 Spring 中的 JMS 接收消息。 #### 4.3.1.同步接收 虽然 JMS 通常与异步处理相关联,但你可以同步地使用消息。重载的`receive(..)`方法提供了此功能。在同步接收期间,调用线程会阻塞消息,直到消息变得可用。这可能是一个危险的操作,因为调用线程可能会无限期地被阻塞。`receiveTimeout`属性指定接收者在放弃等待消息之前应该等待多长时间。 #### 4.3.2.异步接收:消息驱动的 POJO | |Spring 还通过使用`@JmsListener`注释来支持带注释的侦听器端点,并提供一种以编程方式注册端点的开放基础设施。
这是迄今为止设置异步接收器的最方便的方式。
有关更多详细信息,请参见[启用监听器端点注释](#jms-annotated-support)。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Bean(MDB)在 EJB 世界中,以类似于消息驱动的方式,消息驱动的 POJO 充当 JMS 消息的接收者。MDP 上的一个限制(但请参见[Using `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`)的消息侦听器容器之一: ``` ``` 参见各种消息侦听器容器(所有这些容器实现[MessagelistenerContainer](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/listener/MessageListenerContainer.html))的 Spring Javadoc,以获得每个实现所支持的特性的完整描述。 #### 4.3.3.使用`SessionAwareMessageListener`接口 #### `SessionAwareMessageListener`接口是一个 Spring 特定的接口,它提供了与 JMS`MessageListener`接口类似的契约,但也使消息处理方法能够访问 JMS`Session`,从该接口接收`Message`。下面的清单显示了`SessionAwareMessageListener`接口的定义: ``` package org.springframework.jms.listener; public interface SessionAwareMessageListener { void onMessage(Message message, Session session) throws JMSException; } ``` 如果你希望你的 MDP 能够响应任何接收到的消息(通过使用`onMessage(Message, Session)`方法中提供的`Session`方法),则可以选择让你的 MDP 实现这个接口(优先于标准的 JMS`MessageListener`接口)。 Spring 附带的所有消息侦听器容器实现都支持实现`MessageListener`或 `SessionAwareMessageListener’接口的 MDP。实现“SessionAwareMessageListener”的类需要注意的是,它们随后会通过接口绑定到 Spring。是否使用它的选择完全取决于作为应用程序开发人员或架构师的你。 注意,`onMessage(..)`接口的`SessionAwareMessageListener`方法抛出`JMSException`。与标准的 JMS`MessageListener`接口相反,当使用`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: ``` ``` 下一个示例展示了另一个只能处理接收 JMS`TextMessage’消息的 MDP。请注意消息处理方法实际上是如何被称为“receive”(在`MessageListenerAdapter`中消息处理方法的名称默认为`handleMessage`)的,但是它是可配置的(正如你在本节后面看到的)。还请注意`receive(..)`方法是如何强类型的,以便仅接收和响应 JMS 的“textmessage”消息。下面的清单显示了`TextMessageDelegate`接口的定义: ``` public interface TextMessageDelegate { void receive(TextMessage message); } ``` 下面的清单显示了一个实现`TextMessageDelegate`接口的类: ``` public class DefaultTextMessageDelegate implements TextMessageDelegate { // implementation elided for clarity... } ``` 然后,伴随函数`MessageListenerAdapter`的配置如下: ``` ``` 请注意,如果`messageListener`接收到不是`TextMessage`的类型的 JMS`Message`,则抛出一个`IllegalStateException`(并随后吞下)。`MessageListenerAdapter`类的另一个功能是,如果处理程序方法返回一个非 void 值,则能够自动发送回响应`Message`。考虑以下接口和类: ``` public interface ResponsiveTextMessageDelegate { // notice the return type... String receive(TextMessage message); } ``` ``` public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate { // implementation elided for clarity... } ``` 如果将`DefaultResponsiveTextMessageDelegate`与 `MessageListenerAdapter’一起使用,则(在默认配置中)将执行`'receive(..)'`方法返回的任何非空值转换为 `textmessage’。然后将结果`TextMessage`发送到在原始`Message`的 JMS`Reply-To`属性中定义的`Destination`(如果存在)或在`MessageListenerAdapter`上设置的默认`Destination`(如果已经配置了)。如果没有找到`Destination`,则抛出一个`InvalidDestinationException`(请注意,此异常不会被吞没并向上传播调用堆栈)。 #### 4.3.5.处理事务中的消息 在事务中调用消息侦听器只需要重新配置侦听器容器。 你可以通过侦听器容器定义上的`sessionTransacted`标志激活本地资源事务。然后,每个消息侦听器调用都在一个活动的 JMS 事务中进行操作,在侦听器执行失败的情况下,消息接收将被回滚。发送响应消息(通过`SessionAwareMessageListener`)是同一本地事务的一部分,但任何其他资源操作(例如数据库访问)都是独立操作的。这通常需要在侦听器实现中进行重复消息检测,以覆盖数据库处理已提交但消息处理未提交的情况。 考虑以下 Bean 定义: ``` ``` 要参与外部管理的事务,你需要配置一个事务管理器,并使用一个支持外部管理事务的侦听器容器(通常是`DefaultMessageListenerContainer`)。 要为 XA 事务参与配置消息侦听器容器,你需要配置`JtaTransactionManager`(默认情况下,它将委托给 Java EE 服务器的事务子系统)。请注意,底层 JMS`ConnectionFactory`需要具有 XA 功能,并正确地向你的 JTA 事务协调器注册。(检查你的 Java EE 服务器对 JNDI 资源的配置。)这使得消息接收以及(例如)数据库访问成为同一事务的一部分(使用统一的提交语义,以 XA 事务日志开销为代价)。 Bean 以下定义创建了事务管理器: ``` ``` 然后,我们需要将其添加到我们先前的容器配置中。剩下的就交给集装箱了。下面的示例展示了如何做到这一点: ``` (1) ``` |**1**|我们的交易经理。| |-----|------------------------| ### 4.4.对 JCA 消息端点的支持 从版本 2.5 开始, Spring 还提供了对基于 JCA 的“MessageListener”容器的支持。`JmsMessageEndpointManager`尝试从提供者的“ResourceAdapter”类名中自动确定`ActivationSpec`类名。因此,通常可以提供 Spring 的泛型`JmsActivationSpecConfig`,如下例所示: ``` ``` 或者,你可以使用给定的“ActivationSpec”对象设置`JmsMessageEndpointManager`。`ActivationSpec`对象也可以来自 JNDI 查找(使用``)。下面的示例展示了如何做到这一点: ``` ``` 使用 Spring 的`ResourceAdapterFactoryBean`,可以在本地配置目标`ResourceAdapter`,如下例所示: ``` ``` 指定的`WorkManager`还可以指向特定于环境的线程池——通常通过`SimpleTaskWorkManager`实例的`asyncTaskExecutor`属性。考虑为所有`ResourceAdapter`实例定义一个共享线程池,如果你碰巧使用了多个适配器。 在某些环境中(例如 WebLogic9 或更高),你可以从 JNDI 获得整个`ResourceAdapter`对象(通过使用``)。然后,基于 Spring 的消息侦听器可以与服务器托管的`ResourceAdapter`进行交互,该服务器还使用服务器内置的`WorkManager`。 有关更多详细信息,请参见[JMSMessageEndPointManager’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.html)、[JMSActivationSpecconfig](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.html)和[“资源适应性工厂”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jca/support/ResourceAdapterFactoryBean.html)的 Javadoc。 Spring 还提供了不绑定到 JMS 的通用 JCA 消息端点管理器:`org.SpringFramework.jca.endpoint.GenericMessageEndpointManager`。该组件允许使用任何消息侦听器类型(例如 JMS`MessageListener`)和任何特定于提供者的`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’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/annotation/JmsListenerConfigurer.html)的类的 Javadoc。 如果你更喜欢[XML 配置](#jms-namespace),那么可以使用``元素,如下例所示: ``` ``` #### 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 端点中注入的主要元素如下: * RAW`javax.jms.Message`或其任一子类(前提是它匹配传入的消息类型)。 * `javax.jms.Session`用于对本机 JMS API 的可选访问(例如,用于发送自定义回复)。 * 表示传入的 JMS 消息的`org.springframework.messaging.Message`。请注意,此消息同时包含自定义标题和标准标题(由`JmsHeaders`定义)。 * `@Header`-带注释的方法参数,以提取特定的标头值,包括标准的 JMS 标头。 * 一个`@Headers`-带注释的参数,该参数也必须分配给`java.util.Map`,以获得对所有标题的访问权限。 * 不属于受支持类型之一(“消息”或“会话”)的未注释元素被视为有效负载。你可以通过使用`@Payload`对参数进行注释来使其显式。你还可以通过添加一个额外的“@valid”来打开验证。 注入 Spring 的`Message`抽象的能力对于受益于存储在特定传输消息中的所有信息而不依赖特定传输 API 特别有用。下面的示例展示了如何做到这一点: ``` @JmsListener(destination = "myDestination") public void processOrder(Message 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 processOrder(Order order) { // order processing return MessageBuilder .withPayload(status) .setHeader("code", 1234) .build(); } ``` 如果需要在运行时计算响应目的地,可以将响应封装在`JmsResponse`实例中,该实例还提供了在运行时使用的目的地。我们可以将前面的示例改写如下: ``` @JmsListener(destination = "myDestination") public JmsResponse> processOrder(Order order) { // order processing Message 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 模式,如下例所示: ``` ``` |**1**|引用 JMS 模式。| |-----|---------------------------| 名称空间由三个顶级元素组成:``、``和``。``启用[注释驱动的监听器端点](#jms-annotated)。``和``定义共享侦听器容器配置,并可以包含``子元素。下面的示例展示了两个侦听器的基本配置: ``` ``` 前面的示例相当于创建两个不同的侦听器容器 Bean 定义和两个不同的`MessageListenerAdapter` Bean 定义,如[Using `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`)。请注意,指定的最小值只是一个提示
,在运行时可能会被忽略。默认值是容器提供的值。| ``元素还接受几个可选属性。这允许定制各种策略(例如,`taskExecutor`和 `DestinationResolver’)以及基本的 JMS 设置和资源引用。通过使用这些属性,你可以定义高度定制的侦听器容器,同时仍然受益于名称空间的便利。 你可以通过指定 Bean 的`id`来通过`factory-id`属性自动将此类设置公开为`JmsListenerContainerFactory`,如下例所示: ``` ``` 下表描述了所有可用的属性。有关单个属性的更多详细信息,请参见[`AbstractMessagelistenerContainer’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/listener/AbstractMessageListenerContainer.html)的类级 Javadoc 及其具体的子类。Javadoc 还提供了对事务选择和消息重新交付场景的讨论。 | Attribute |说明| |---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `container-type` |此侦听器容器的类型。可用的选项是`default`,`simple`,`default102’,或`simple102`(默认选项是`default`)。| | `container-class` |根据`container-type`属性,默认值是 Spring 的标准`DefaultMessageListenerContainer`或 `SimpleMessageListenerContainer’。| | `factory-id` |使用指定的`id`将此元素定义的设置公开为`JmsListenerContainerFactory`,以便它们可以在其他端点上重用。| | `connection-factory` |引用 JMS`ConnectionFactory` Bean(缺省的 Bean 名称是“ConnectionFactory”)。| | `task-executor` |对 Spring `TaskExecutor`的引用,用于 JMS 侦听器调用程序。| | `destination-resolver` |引用`DestinationResolver`解决 JMS`Destination`实例的策略。| | `message-converter` |引用用于将 JMS 消息转换为侦听器
方法参数的`MessageConverter`策略。默认值是`SimpleMessageConverter`。| | `error-handler` |对`ErrorHandler`策略的引用,该策略用于处理在
执行过程中可能发生的`MessageListener`未捕获的异常。| | `destination-type` |此侦听器的 JMS 目标类型:`queue`,`topic`,`durableTopic`,`sharedTopic`,
或`sharedDurableTopic`。这可能启用容器的`pubSubDomain`、`subscriptionDurable`和`subscriptionShared`属性。默认值是`queue`(禁用
这三个属性)。| |`response-destination-type`|响应的 JMS 目标类型:`queue`或`topic`。缺省值是“destination-type”属性的值。| | `client-id` |此侦听器容器的 JMS 客户机 ID。在使用
持久订阅时,必须指定它。| | `cache` |JMS 资源的缓存级别:`none`,`connection`,`session`,`consumer`,或 `auto’。默认情况下,缓存级别实际上是`consumer`,除非已经指定了外部事务管理器——在这种情况下,有效的
默认值将是`none`(假设 Java EE 风格的事务管理,其中给定的
ConnectionFactory 是一个 XA-aware 池)。| | `acknowledge` |本机 JMS 确认模式:`auto`,`client`,`dups-ok`,或`transacted`。值
的`transacted`激活局部交易的`Session`。作为一种选择,你可以指定
`transaction-manager`属性,稍后将在表中进行说明。默认值为`auto`。| | `transaction-manager` |对外部`PlatformTransactionManager`的引用(通常是基于 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` |指定恢复尝试之间的间隔(以毫秒为单位)。它提供了一种方便的
方法来创建具有指定间隔的`FixedBackOff`。对于更多的恢复
选项,可以考虑指定一个`BackOff`实例。默认值是 5000 毫秒
(即 5 秒)。| | `phase` |此容器应在其中启动和停止的生命周期阶段。
值越低,这个容器启动得越早,停止得越晚。默认值是 `integer.max_value’,这意味着容器尽可能晚地启动,并尽可能快地以
的形式停止。| 使用`jms`模式支持配置基于 JCA 的侦听器容器非常相似,如下例所示: ``` ``` 下表描述了 JCA 变体的可用配置选项: | Attribute |说明| |---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `factory-id` |使用指定的`id`将此元素定义的设置公开为`JmsListenerContainerFactory`,以便它们可以在其他端点上重用。| | `resource-adapter` |引用 JCA`ResourceAdapter` 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/DefaultJmsActivationSpecFactory.html))。| | `destination-resolver` |引用`DestinationResolver`解析 JMS 的策略`Destinations`。| | `message-converter` |引用用于将 JMS 消息转换为侦听器
方法参数的`MessageConverter`策略。默认值为`SimpleMessageConverter`。| | `destination-type` |此侦听器的 JMS 目标类型:`queue`,`topic`,`durableTopic`,`sharedTopic`。
或`sharedDurableTopic`。这可能启用容器的`pubSubDomain`、`subscriptionDurable`、
和`subscriptionShared`属性。默认值是`queue`(禁用
这三个属性)。| |`response-destination-type`|响应的 JMS 目标类型:`queue`或`MBeanExporter`。缺省值是“destination-type”属性的值。| | `client-id` |此侦听器容器的 JMS 客户机 ID。在使用
持久订阅时需要指定它。| | `acknowledge` |本机 JMS 确认模式:`auto`,`client`,`dups-ok`,或`transacted`。值
的`transacted`激活了局部交易的`Session`。作为一种选择,你可以指定
后面描述的`transaction-manager`属性。默认值为`auto`。| | `transaction-manager` |对 Spring `JtaTransactionManager`或 `javax.transactionmanager’的引用,用于为每个
传入消息启动 XA 事务。如果未指定,则使用本机确认(请参见“确认”属性)。| | `concurrency` |为每个侦听器启动的并发会话或消费者的数量。它可以是
表示最大值的简单数字(例如`5`),也可以是表示
的下限和上限的范围(例如,`3-5`)。请注意,指定的最小值只是一个
提示,并且在使用 JCA 侦听器容器时,该提示通常在运行时被忽略。
默认值为 1。| | `prefetch` |要加载到单个会话中的消息的最大数量。请注意,提高这个
值可能会导致并发消费者的饥饿。| ## 5. JMX Spring 中的 JMX(Java 管理扩展)支持提供了一些特性,这些特性使你能够轻松且透明地将你的 Spring 应用程序集成到 JMX 基础设施中。 JMX? 本章不是对 JMX 的介绍。它并不试图解释为什么你可能想要使用 JMX。如果你是 JMX 的新手,请参阅本章末尾的[更多资源](#jmx-resources)。 具体来说, 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`MBeanServer`。例如,考虑以下类: ``` package org.springframework.jmx; public class JmxTestBean implements IJmxTestBean { private String name; private int age; private boolean isSuperman; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public void setName(String name) { this.name = name; } public String getName() { return name; } public int add(int x, int y) { return x + y; } public void dontExposeMe() { throw new RuntimeException(); } } ``` 要将此 Bean 的属性和方法公开为 MBean 的属性和操作,可以在配置文件中配置`MBeanExporter`类的实例,并在 Bean 中传递,如下例所示: ``` ``` 来自前面的配置片段的相关 Bean 定义是`exporter` Bean。`beans`属性告诉`MBeanExporter`确切地说,哪些 bean 必须导出到 JMX`MBeanServer`。在默认配置中,`beans``Map`中的每个条目的键被用作对应的条目所引用的 Bean 的[gt r=“989”/>的键。你可以更改此行为,如[Controlling `ObjectName` Instances for Your Beans](#jmx-naming)中所述。 通过这种配置,`testBean` Bean 将作为一个 MBean 在 `objectName``bean:name=testBean1`下公开。默认情况下, Bean 的所有`public`属性作为属性公开,所有`public`方法(从 `object’类继承的方法除外)作为操作公开。 | |`MBeanExporter`是`Lifecycle` Bean(见[启动和关闭回调](core.html#beans-factory-lifecycle-processor))。默认情况下,在
应用程序生命周期期间,MBean 会尽可能晚地导出。你可以配置`phase`的
导出,或者通过设置`autoStartup`标志禁用自动注册。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.1.1.创建一个 mBeanServer `@ManagedOperationParameter`中显示的配置假定应用程序运行在一个已经运行`MBeanServer`的环境中。在这种情况下, Spring 尝试定位正在运行的`MBeanServer`,并向该服务器注册你的 bean(如果有的话)。当你的应用程序运行在具有自己的`MBeanServer`的容器(例如 Tomcat 或 IBMWebSphere)内时,这种行为非常有用。 但是,这种方法在独立环境中或在不提供`MBeanServer`的容器中运行时没有用。为了解决这个问题,你可以通过在配置中添加 `org.springframework.jmx.support.mbeanserverfactorybean’类的实例来声明性地创建一个 `mBeanServer’实例。通过将 `mBeanExporter’实例的`server`属性的值设置为 `mBeanServerFactoryBean’返回的`MBeanServer`值,还可以确保使用特定的`MBeanServer`,如下例所示: ``` ``` 在前面的示例中,`MBeanServer`的实例由`MBeanServerFactoryBean`创建,并通过`MBeanExporter`属性提供给`MBeanExporter`。当你提供自己的“mBeanServer”实例时,`MBeanExporter`不会尝试定位正在运行的“mBeanServer”,而是使用提供的`MBeanServer`实例。为了正确地实现这一点,你必须在 Classpath 上有一个 JMX 实现。 #### 5.1.2.重用现有的`MBeanServer` 如果没有指定服务器,`MBeanExporter`将尝试自动检测正在运行的 `mBeanServer’。这在大多数环境中都有效,其中只使用了一个`MBeanServer`实例。但是,当存在多个实例时,输出者可能会选择错误的服务器。在这种情况下,你应该使用`MBeanServer``agentId`来指示要使用哪个实例,如下例所示: ``` ... ``` 对于现有的`MBeanServer`具有通过查找方法检索的动态(或未知)`agentid’的平台或情况,你应该使用[factory-method](core.html#beans-factory-class-static-factory-method),如下例所示: ``` ``` #### 5.1.3.惰性初始化的 MBean 如果你配置的 Bean 带有`MBeanExporter`,该配置也用于延迟初始化,则`MBeanExporter`不会破坏此契约,并避免实例化 Bean。相反,它使用`MBeanServer`注册一个代理,并将从容器获得 Bean 的时间推迟到对代理进行第一次调用时。 #### 5.1.4.MBeans 的自动注册 通过`MBeanExporter`导出并且已经是有效的 MBean 的任何 bean 都以原样在`MBeanServer`中注册,而不需要 Spring 的进一步干预。通过将`autodetect`属性设置为`true`,可以使`MBeanExporter`自动检测到 MBean,如下例所示: ``` ``` 在前面的示例中,被称为`spring:mbean=true`的 Bean 已经是一个有效的 JMX MBean,并且由 Spring 自动注册。默认情况下,自动检测 JMX 注册的 Bean 的 Bean 名称用作`ObjectName`。你可以重写此行为,详见[Controlling `ObjectName` Instances for Your Beans](#jmx-naming)。 #### 5.1.5.控制注册行为 考虑 Spring `MBeanExporter`通过使用`ObjectName``autodetect`尝试用`MBeanServer`注册`MBean`的场景。如果`server`实例已在同一个`ObjectName`下注册,则默认行为是失败(并抛出`InstanceAlreadyExistsException`)。 你可以精确地控制当`MBean`被注册为`MBeanServer`时会发生什么。 Spring 的 JMX 支持允许三种不同的注册行为,以在注册过程发现`MBean`已经在相同的`ObjectName`下注册时控制注册行为。下表总结了这些注册行为: |Registration behavior|解释| |---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `FAIL_ON_EXISTING` |这是默认的注册行为。如果`MBean`实例已经在同一个
下注册了,则正在注册的`MBean`不是
注册的,则抛出一个`InstanceAlreadyExistsException`。现有的“MBean”不受影响。| | `IGNORE_EXISTING` |如果`MBean`实例已在同一个`ObjectName`下注册,则未注册正在注册的 `mbean’。现有的`MBean`是
不受影响的,并且不会抛出`Exception`。这在设置
多个应用程序希望在共享的`MBean`中共享一个公共`MBean`时很有用。| | `REPLACE_EXISTING` |如果`MBean`实例已经在同一个`ObjectName`下注册,则先前注册的
现有的
未注册,并且新的 `mbean’已在其位置注册(新的`Notification`有效地替换了
以前的实例)。| 上表中的值被定义为`JmxTestBean`类上的枚举。如果要更改默认的注册行为,则需要将`MBeanExporter`定义中的 `registrationPolicy’属性的值设置为其中一个值。 下面的示例展示了如何从默认注册行为更改为`REPLACE_EXISTING`行为: ``` ``` ### 5.2.控制 bean 的管理界面 在[前一节](#jmx-exporting-registration-behavior)中的示例中,你几乎无法控制 Bean 的管理接口。 Bean 每个导出的`public`属性和方法都分别作为 JMX 属性和操作公开。 Spring JMX 提供了一种全面的、可扩展的机制,用于控制你的 bean 的管理接口,从而实现对你导出的 bean 的哪些属性和方法作为 JMX 属性和操作实际公开的更精细的控制。 #### 5.2.1.使用`MBeanInfoAssembler`接口 在幕后,`MBeanExporter`委托给 `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`才能使其正常工作(没有默认值)。 要将 Bean 标记为导出到 JMX,你应该使用“ManagedResource”注释对 Bean 类进行注释。你必须用`ManagedOperation`注释将希望公开的每个方法标记为一个操作,并用`ManagedAttribute`注释标记希望公开的每个属性。在标记属性时,可以省略 getter 或 setter 的注释,以分别创建只写或只读属性。 | |带有注释的 Bean 的`ManagedResource`必须是公共的,就像公开
操作或属性的方法一样。| |---|-----------------------------------------------------------------------------------------------------------------| 下面的示例显示了我们在[创建一个 mBeanServer](#jmx-exporting-mbeanserver)中使用的`MBean`类的注释版本: ``` 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`注释配置了一组属性。这些属性可用于配置由[源级元数据类型](#jmx-interface-metadata-types)生成的 MBean 的各个方面,并在后面的[源级元数据类型](#jmx-interface-metadata-types)中进行更详细的说明。 `age`和`name`属性都使用`ManagedAttribute`注释,但是,在`name`属性的情况下,只标记 getter。这导致这两个属性都作为属性包含在管理接口中,但是`age`属性是只读的。 最后,`add(int, int)`方法被标记为`ManagedOperation`属性,而`dontExposeMe()`方法则不是。当你使用`MetadataMBeanInfoAssembler`时,这将导致管理 INT 面只包含一个操作(`add(INT,int)`)。 下面的配置显示了如何配置`MBeanExporter`以使用 `MetadatambeanInfoAssembler’: ``` ``` 在前面的示例中,`MetadataMBeanInfoAssembler` Bean 已配置了`AnnotationJmxAttributeSource`类的实例,并通过 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`和[MetadatanamingStrategy](#jmx-naming-metadata)| 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. |`ManagedResource`,`ManagedAttribute`,`ManagedOperation`,或`ManagedOperationParameter`| |`currencyTimeLimit`| Sets the value of the `currencyTimeLimit` descriptor field. |`ManagedResource`或`ManagedAttribute`| | `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 资源的自动检测的支持。如果你使用`AutodetectCapableMBeanInfoAssembler`实例配置 `mbeanexporter’,则允许它对包含用于暴露于 JMX 的 bean 进行“投票”。 `AutodetectCapableMBeanInfo`接口的唯一实现是`serverConnector`,它投票支持包含标记有`ManagedResource`属性的任何 Bean。在这种情况下,默认的方法是使用 Bean 名称作为`ObjectName`,这将导致类似于以下配置的结果: ``` ``` 注意,在前面的配置中,没有向`MBeanExporter`传递任何 bean。但是,`JmxTestBean`仍然是注册的,因为它被标记为`ManagedResource`属性,并且`MetadataMBeanInfoAssembler`检测到并投票包含它。这种方法的唯一问题是`JmxTestBean`的名称现在具有业务含义。你可以通过更改`ObjectName`中定义的[Controlling `ObjectName` Instances for Your Beans](#jmx-naming)创建的默认行为来解决此问题。 #### 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 以使用此接口作为管理接口的定义: ``` org.springframework.jmx.IJmxTestBean ``` 在前面的示例中,`InterfaceBasedMBeanInfoAssembler`被配置为在为任何 Bean 构建管理接口时使用 `IJMXTestBean’接口。理解由`InterfaceBasedMBeanInfoAssembler`处理的 bean 并不是实现用于生成 JMX 管理接口的接口所必需的,这一点很重要。 在前面的例子中,`IJmxTestBean`接口用于为所有 bean 构造所有管理接口。在许多情况下,这不是所需的行为,你可能希望为不同的 bean 使用不同的接口。在这种情况下,可以通过`interfaceMappings`属性传递 `InterfaceBasedMBeanInfoAssembler`a`Properties`实例,其中每个条目的键是 Bean 名称,每个条目的值是一个逗号分隔的接口名称列表,用于 Bean。 如果没有通过`managedInterfaces`或 `interfaceppings’属性指定管理接口,则`InterfaceBasedMBeanInfoAssembler`将反映在 Bean 上,并使用由 Bean 实现的所有接口来创建管理接口。 #### 5.2.6.使用`MethodNameBasedMBeanInfoAssembler` `MethodNameBasedMBeanInfoAssembler`允许你指定作为属性和操作公开给 JMX 的方法名列表。下面的代码展示了一个示例配置: ``` add,myOperation,getName,setName,getAge ``` 在前面的示例中,可以看到`add`和`myOperation`方法作为 JMX 操作公开,`getName()`、`setName(String)`和`getAge()`作为 JMX 属性的适当部分公开。在前面的代码中,方法映射应用于公开给 JMX 的 bean。要在 Bean-by- Bean 的基础上控制方法曝光,可以使用`methodMappings`的`MethodNameMBeanInfoAssembler`属性将 Bean 名称映射到方法名称列表。 ### 5.3.控制 bean 的`ObjectName`实例 在幕后,`MBeanExporter`将委托给 `ObjectNamingStrategy’的一个实现,以获得它所注册的每个 bean 的`ObjectName`实例。默认情况下,默认实现`KeyNamingStrategy`使用 `bean``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 试图在`Properties`中使用与 Bean 键对应的键来定位一个条目。如果没有找到条目,或者`Properties`实例是 `null’,则使用 Bean 键本身。 下面的代码显示了`KeyNamingStrategy`的示例配置: ``` bean:name=testBean1 names1.properties,names2.properties ``` 前面的示例使用`KeyNamingStrategy`实例配置`Properties`实例,该实例是从映射属性定义的`Properties`实例和映射属性定义的路径中的属性文件合并而来的。在这种配置中,`testBean` Bean 给定了`ObjectName`的`bean:name=testBean1`,因为这是`Properties`实例中的条目,该实例具有与 Bean 键对应的键。 如果在`Properties`实例中找不到条目,则 Bean 键名被用作`ObjectName`。 #### 5.3.2.使用`MetadataNamingStrategy` `MetadataNamingStrategy`在每个 Bean 上使用`objectName`属性的`objectName`属性来创建`ObjectName`。下面的代码显示了`MetadataNamingStrategy`的配置: ``` ``` 如果没有为`objectName`属性提供`ManagedResource`,则使用以下格式创建一个 `ObjectName’:`ObjectName`。例如,以下 Bean 生成的`ObjectName`将是 `com。例如:type=myclass,name=mybean`: ``` ``` #### 5.3.3.配置基于注释的 MBean 导出 如果你喜欢使用[基于注释的方法](#jmx-interface-metadata)来定义你的管理接口,那么可以使用`MBeanExporter`的一个方便的子类:`AnnotationmBeanExporter’。在定义这个子类的实例时,你不再需要 `namingstrategy’、`assembler`和`ObjectName`配置,因为它总是使用标准的基于 Java 注释的元数据(也总是启用自动检测)。实际上,与定义`MBeanExporter` Bean 不同的是,`@EnableMBeanExport``@Configuration`注释支持更简单的语法,如下例所示: ``` @Configuration @EnableMBeanExport public class AppConfig { } ``` 如果你更喜欢基于 XML 的配置,则``元素具有相同的目的,如以下清单所示: ``` ``` 如果有必要,可以提供对特定 MBean`server`的引用,并且 `defaultDomain’属性(属性为`AnnotationMBeanExporter`)接受生成的 MBean`ObjectName`域的替换值。正如下面的示例所示,正如在[MetadatanamingStrategy](#jmx-naming-metadata)上一节中所描述的那样,这将代替完全限定的包名: ``` @EnableMBeanExport(server="myMBeanServer", defaultDomain="myDomain") @Configuration ContextConfiguration { } ``` 下面的示例展示了前面的基于注释的示例的 XML 等价物: ``` ``` | |不要在 Bean 类中结合自动检测 JMX
注释使用基于接口的 AOP 代理。基于接口的代理“隐藏”目标类,它
还隐藏 JMX 管理的资源注释。因此,你应该在
的情况下使用目标类代理(通过在[MetadatanamingStrategy](#jmx-naming-metadata)、“上设置”proxy-target-class" 标志,以此类推)。否则,在
启动时,你的 JMX Bean 可能会被忽略。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 5.4.使用 JSR-160 连接器 对于远程访问, Spring JMX 模块在 `org.springframework.jmx.support` 包中提供了两个`FactoryBean`实现,用于创建服务器端和客户端连接器。 #### 5.4.1.服务器端连接器 要让 Spring JMX 创建、启动和公开一个 JSR-160`JMXConnectorServer`,你可以使用以下配置: ``` ``` 默认情况下,`ConnectorServerFactoryBean`创建一个`JMXConnectorServer`绑定到 `service:jmx:jmxmp://localhost:9875’的`JMXConnectorServer`。因此,`serverConnector` Bean 通过 LocalHost 上的 JMXMP 协议(端口 9875)将本地`MBeanServer`公开给客户端。请注意,JMXMP 协议在 JSR160 规范中被标记为可选的。目前,主要的开源 JMX 实现 MX4J 和 JDK 提供的 JDJ 都不支持 JMXMP。 要指定另一个 URL 并将`JMXConnectorServer`本身注册到 `mBeanServer’中,你可以分别使用和`ObjectName`属性,如下例所示: ``` ``` 如果设置了`ObjectName`属性, Spring 会自动在`ObjectName`下用`MBeanServer`注册连接器。下面的示例显示了在创建 `JMXConnector’时可以传递给`ConnectorServerFactoryBean`的完整参数集: ``` ``` 请注意,当你使用基于 RMI 的连接器时,需要启动查找服务(“tnameserv”或“rmiRegistry”)才能完成名称注册。如果使用 Spring 通过 RMI 为你导出远程服务,则 Spring 已经构建了 RMI 注册中心。如果没有,你可以通过使用以下配置片段轻松启动注册表: ``` ``` #### 5.4.2.客户端连接器 要为远程 JSR-160 启用的`MBeanServerConnection`创建`MBeanServer`,可以使用 `mBeanServerConnectionFactoryBean`,如下例所示: ``` ``` #### 5.4.3.JMX 超过 Hessian 或 SOAP JSR-160 允许扩展客户机和服务器之间的通信方式。前面几节中所示的示例使用了 JSR-160 规范(IIOP 和 JRMP)和(可选的)JMXMP 所要求的基于 RMI 的强制实现。通过使用其他提供程序或 JMX 实现(例如[MX4J](http://mx4j.sourceforge.net)),你可以利用 SOAP 或 Hessian 等协议而不是简单的 HTTP 或 SSL 等协议,如下例所示: ``` ``` 在前面的示例中,我们使用了 MX4J3.0.0。有关更多信息,请参见官方的 MX4J 文档。 ### 5.5.通过代理访问 MBean Spring JMX 允许你创建将调用重新路由到注册在本地或远程`MBeanServer`中的 MBean 的代理。这些代理为你提供了一个标准的 Java 接口,你可以通过该接口与 MBean 进行交互。下面的代码展示了如何为在本地`MBeanServer`中运行的 MBean 配置代理: ``` ``` 在前面的示例中,你可以看到为在`MimeMessage`的 `ObjectName’下注册的 MBean 创建了一个代理。代理实现的接口集由`proxyInterfaces`属性控制,将这些接口上的方法和属性映射到 MBean 上的操作和属性的规则与`InterfaceBasedMBeanInfoAssembler`使用的规则相同。 `MBeanProxyFactoryBean`可以创建一个代理,该代理可以通过“mBeanServerConnection”访问任何 MBean。默认情况下,定位并使用了本地`MBeanServer`,但你可以重写此内容,并提供指向远程 `mBeanServer’的`MBeanServerConnection`,以满足指向远程 mBean 的代理的需要: ``` ``` 在前面的示例中,我们创建一个`MBeanServerConnection`,它指向使用`MBeanServerConnectionFactoryBean`的远程计算机。然后通过`server`属性将此`MBeanServerConnection`传递给`MBeanProxyFactoryBean`。创建的代理通过这个“mBeanServerConnection”将所有调用转发到`MBeanServer`。 ### 5.6.通知 Spring 的 JMX 产品包括对 JMX 通知的全面支持。 #### 5.6.1.为通知注册监听器 Spring 的 JMX 支持使得在任意数量的 MBean 中注册任意数量的“notificationListener”变得很容易(这包括 Spring 的`MBeanExporter`导出的 MBean 和通过某些其他机制注册的 MBean)。例如,考虑这样的场景:每当目标 MBean 的属性发生变化时,都希望(通过“通知”)获得通知。以下示例将通知写入控制台: ``` 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`: ``` ``` 有了前面的配置,每次从目标 MBean(` Bean:name=TestBean1`)广播一个 JMX时,就会通知通过属性注册为侦听器的 Bean。然后,`ConsoleLoggingNotificationListener` Bean 可以对`Notification`采取它认为适当的任何操作。 还可以使用 Straight Bean names 作为导出的 bean 和侦听器之间的链接,如下例所示: ``` ``` 如果你想为所包含的`MBeanExporter`导出的所有 bean 注册一个`NotificationListener`实例,则可以使用特殊通配符作为`notificationListenerMappings`属性映射中一个条目的键,如下例所示: ``` ``` 如果需要执行逆操作(即针对 MBean 注册多个不同的侦听器),则必须使用`notificationListeners`list 属性(优先于`notificationListenerMappings`属性)。这一次,我们不再为单个 MBean 配置`NotificationListener`,而是配置 `NotificationListenerBean’实例。a`NotificationListenerBean`封装了一个 `notificationListener’和将在`MBeanServer`中注册的`ObjectName`(或`ObjectNames`)。`NotificationListenerBean`还封装了许多其他属性,例如`NotificationFilter`和可以在高级 JMX 通知场景中使用的任意 handback 对象。 使用`NotificationListenerBean`实例时的配置与前面介绍的配置没有很大不同,如下例所示: ``` bean:name=testBean1 ``` 前面的示例与第一个通知示例等价。然后,假设我们希望在每次提出`Notification`时都给出一个 handback 对象,并且我们还希望通过提供 `notificationfilter’过滤掉无关的`Notifications`。下面的示例实现了这些目标: ``` bean:name=testBean1 bean:name=testBean2 ``` (关于什么是 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/notification/NotificationPublisher.html)接口的 Javadoc 中所述,通过`NotificationPublisher`机制发布事件的托管 bean 不负责通知侦听器的状态管理。 Spring 的 JMX 支持负责处理所有的 JMX 基础设施问题。作为应用程序开发人员,你所需要做的就是实现“NotificationPublisherAware”接口,并通过使用提供的`NotificationPublisher`实例开始发布事件。请注意,`NotificationPublisher`是在托管 Bean 已注册为`MBeanServer`之后设置的。 使用`NotificationPublisher`实例非常简单。你创建一个 JMX`Notification’实例(或一个适当的`Notification`子类的实例),用与将要发布的事件相关的数据填充通知,并在 `NotificationPublisher’实例上调用`sendNotification(Notification)`,传入`Notification`。 在下面的示例中,每当调用`add(int, int)`操作时,导出的`JmxTestBean`实例都会发布一个 `NotificationEvent’: ``` package org.springframework.jmx; import org.springframework.jmx.export.notification.NotificationPublisherAware; import org.springframework.jmx.export.notification.NotificationPublisher; import javax.management.Notification; public class JmxTestBean implements IJmxTestBean, NotificationPublisherAware { private String name; private int age; private boolean isSuperman; private NotificationPublisher publisher; // other getters and setters omitted for clarity public int add(int x, int y) { int answer = x + y; this.publisher.sendNotification(new Notification("add", this, 0)); return answer; } public void dontExposeMe() { throw new RuntimeException(); } public void setNotificationPublisher(NotificationPublisher notificationPublisher) { this.publisher = notificationPublisher; } } ``` `NotificationPublisher`接口和使其全部工作的机制是 Spring 的 JMX 支持的更好的功能之一。然而,它确实带来了将类与 Spring 和 JMX 耦合的代价。和往常一样,这里的建议是要务实。如果你需要`Notifications`提供的功能,并且可以接受 Spring 和 JMX 的耦合,那么就这样做。 ### 5.7.更多资源 本节包含指向有关 JMX 的更多资源的链接: * 甲骨文的[JMX homepage](https://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html)。 * [JMX 规范](https://jcp.org/aboutJava/communityprocess/final/jsr003/index3.html)(jsr-000003)。 * [JMX 远程 API 规范](https://jcp.org/aboutJava/communityprocess/final/jsr160/index.html)(jsr-000160)。 * [MX4J homepage](http://mx4j.sourceforge.net/)。(MX4J 是各种 JMX 规范的开放源代码实现。 ## 6. 电子邮件 本节描述了如何使用 Spring 框架发送电子邮件。 库依赖项 为了使用 Spring 框架的电子邮件库,需要在你的应用程序的 Classpath 上设置以下 jar: * [Javamail/Jakarta Mail1.6](https://eclipse-ee4j.github.io/mail/)库 该图书馆可在网上免费查阅——例如,在 Maven Central 以 `com.sun.mail:jakarta.mail’的形式提供。请确保使用最新的 1.6.x 版本,而不是 Jakarta Mail2.0(它带有不同的包名称空间)。 Spring 框架提供了用于发送电子邮件的有用的实用库,该实用库保护你不受底层邮件系统的详细信息的影响,并代表客户机负责低级别的资源处理。 `org.springframework.mail`包是 Spring 框架电子邮件支持的根级别包。发送电子邮件的中心接口是`MailSender`接口。一个简单的值对象封装了一个简单邮件的属性,如`from`和`to`(加上许多其他的)是`SimpleMailMessage`类。这个包还包含一个检查异常的层次结构,它们提供了比较低级别邮件系统异常更高级别的抽象,根异常是“MailException”。有关 Rich Mail 异常层次结构的更多信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/mail/MailException.html)。 `MailSender`接口增加了专门的 JavaMail 功能,例如对`MailSender`接口(它从该接口继承而来)的 MIME 消息支持。`JavaMailSender`还提供了一个名为 `org.springframework.mail.javamail.mimeMessagePreparator` 的回调接口,用于准备`MimeMessage`。 ### 6.1.用法 假设我们有一个名为`OrderManager`的业务接口,如下例所示: ``` public interface OrderManager { void placeOrder(Order order); } ``` 进一步假设我们有一个要求,说明需要生成带有订单号的电子邮件消息,并将其发送给下了相关订单的客户。 #### 6.1.1.基本`MailSender`和`SimpleMailMessage`用法 下面的示例展示了如何在有人下订单时使用`MailSender`和`SimpleMailMessage`发送电子邮件: ``` 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 上述代码的定义: ``` ``` #### 6.1.2.使用`JavaMailSender`和`MimeMessagePreparator` 本节描述`OrderManager`的另一个实现,它使用`MimeMessagePreparator`回调接口。在下面的示例中,`mailSender`属性的类型为 `JavaMailSender’,因此我们能够使用 JavaMail`MimeMessage`类: ``` import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage; import org.springframework.mail.MailException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessagePreparator; public class SimpleOrderManager implements OrderManager { private JavaMailSender mailSender; public void setMailSender(JavaMailSender mailSender) { this.mailSender = mailSender; } public void placeOrder(final Order order) { // Do the business calculations... // Call the collaborators to persist the order... MimeMessagePreparator preparator = new MimeMessagePreparator() { public void prepare(MimeMessage mimeMessage) throws Exception { mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress(order.getCustomer().getEmailAddress())); mimeMessage.setFrom(new InternetAddress("[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](core.html#aop)的候选项,然后在
目标上的适当接入点上运行`OrderManager`。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Spring 框架的邮件支持附带标准的 JavaMail 实现。有关更多信息,请参见相关的 Javadoc。 ### 6.2.使用 JavaMail`MimeMessageHelper` 在处理 JavaMail 消息时,一个非常有用的类是 `org.springframework.mail.javamail.mimeMessageHelper’,它使你不必使用冗长的 JavaMail API。使用`MimeMessageHelper`,很容易创建`MimeMessage`,如下例所示: ``` // of course you would use DI in any real-world cases JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setHost("mail.host.com"); MimeMessage message = sender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message); helper.setTo("[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("", 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-ID`(在上面的示例中为 `identifier1234’),将内联资源添加到`MimeMessage`中。添加文本
的顺序和资源是非常重要的。一定要先添加文本,然后`MBeanServerConnection`资源。如果你是在做相反的方式,它是不起作用的。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 6.2.2.使用模板库创建电子邮件内容 前面几节所示示例中的代码通过使用`message.setText(..)`等方法调用,显式地创建了电子邮件消息的内容。这对于简单的情况很好,在前面提到的示例的上下文中也是可以的,其目的是向你展示 API 的基本知识。 然而,在典型的 Enterprise 应用程序中,开发人员通常不会使用前面所示的方法来创建电子邮件消息的内容,原因有很多: * 在 Java 代码中创建基于 HTML 的电子邮件内容是乏味且容易出错的。 * 显示逻辑和业务逻辑之间没有明确的区分。 * 更改电子邮件内容的显示结构需要编写 Java 代码、重新编译、重新部署等等。 通常,解决这些问题的方法是使用模板库(例如 Freemarker)来定义电子邮件内容的显示结构。这使得你的代码只负责创建要在电子邮件模板中呈现的数据并发送电子邮件。当你的电子邮件内容变得相当复杂时,这绝对是一种最佳实践,而且,有了 Spring 框架对 Freemarker 的支持类,这变得非常容易做到。 ## 7. 任务执行和调度 Spring 框架提供了用于分别使用`TaskExecutor`和`TaskScheduler`接口的任务的异步执行和调度的抽象。 Spring 还具有在应用服务器环境中支持线程池或委托给 CommonJ 的那些接口的实现的特征。最终,在公共接口后面使用这些实现,可以抽象出 Java SE5、Java SE6 和 Java EE 环境之间的差异。 Spring 还具有集成类以支持与`Timer`(自 1.3 起的 JDK 的一部分)和 Quartz 调度器([https://www.quartz-scheduler.org/](https://www.quartz-scheduler.org/))的调度。你可以通过使用`FactoryBean`和可选引用“计时器”或`Trigger`实例来分别设置这两个调度程序。此外,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`实例的适配器。有一种替代方法将`Executor`配置参数公开为 Bean 属性。很少需要直接使用“concurrenttaskexecutor”。但是,如果`ThreadPoolTaskExecutor`不够灵活以满足你的需要,`ConcurrentTaskExecutor`是一种替代方案。 * `ThreadPoolTaskExecutor`:这种实现是最常用的。它公开用于配置`java.util.concurrent.ThreadPoolExecutor`的 Bean 属性,并将其包装在`TaskExecutor`中。如果你需要适应不同类型的`java.util.concurrent.Executor`,我们建议你使用`ConcurrentTaskExecutor`代替。 * `WorkManagerTaskExecutor`:该实现使用 CommonJ`WorkManager`作为其支持服务提供者,并且是在 Spring 应用程序上下文中在 WebLogic 或 WebSphere 上设置基于 CommonJ 的线程池集成的中心便利类。 * `DefaultManagedTaskExecutor`:此实现在 JSR-236 兼容的运行时环境(例如 Java 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 属性: ``` ``` ### 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 表达式](#scheduling-cron-expression)的任务调度。例如,以下任务被安排在每小时 15 分钟后运行,但仅在工作日的 9 到 5 个“营业时间”内运行: ``` scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI")); ``` 另一种实现是`PeriodicTrigger`,它接受一个固定的周期、一个可选的初始延迟值和一个布尔值,以指示该周期应该被解释为固定速率还是固定延迟。由于`TaskScheduler`接口已经定义了以固定速率或固定延迟调度任务的方法,因此只要有可能,就应该直接使用这些方法。“periodictrigger”实现的价值在于,你可以在依赖`Trigger`抽象的组件中使用它。例如,可以方便地允许周期性触发器、基于 CRON 的触发器、甚至定制的触发器实现可互换地使用。这样的组件可以利用依赖注入,这样你就可以在外部配置这样的`Triggers`,因此,可以轻松地修改或扩展它们。 #### 7.2.3.`TaskScheduler`实现 与 Spring 的`TaskExecutor`抽象一样,`TaskScheduler`安排的主要好处是,应用程序的调度需求与部署环境分离。当部署到不应由应用程序本身直接创建线程的应用程序服务器环境时,这种抽象级别特别相关。对于这样的场景, Spring 提供了一个`TimerManagerTaskScheduler`,它在 WebLogic 或 WebSphere 上委托给 Commonj`TimerManager`,以及一个更新的 `DefaultManagedTaskScheduler’,它在 Java EE7+ 环境中委托给一个 JSR-236。两者通常都配置了 JNDI 查找。 每当不需要外部线程管理时,一种更简单的选择是在应用程序内进行本地`ScheduledExecutorService`设置,可以通过 Spring 的`ConcurrentTaskScheduler`进行调整。 Spring 还提供了一个“ThreadPoolTaskScheduler”,它在内部委托给`ScheduledExecutorService`,以便沿着`ThreadPoolTaskExecutor`的行提供公共 Bean 样式的配置。这些变体在宽松的应用程序服务器环境中(尤其是在 Tomcat 和 Jetty 上)对于本地嵌入线程池设置也非常适合。 ### 7.3.对调度和异步执行的注释支持 Spring 为任务调度和异步方法执行提供注释支持。 #### 7.3.1.启用调度注释 要启用对`@Scheduled`和`@Async`注释的支持,你可以在你的`@EnableScheduling`类中添加`@EnableScheduling`和 `@enableasync`,如下例所示: ``` @Configuration @EnableAsync @EnableScheduling public class AppConfig { } ``` 你可以为你的应用程序选择相关的注释。例如,如果只需要支持`@Scheduled`,则可以省略`@EnableAsync`。对于更细粒度的控件,你可以另外实现`SchedulingConfigurer`接口、`AsyncConfigurer`接口,或者两者兼而有之。有关详细信息,请参见[“调度配置器”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/scheduling/annotation/SchedulingConfigurer.html)和[“AsyncConfigurer”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/scheduling/annotation/AsyncConfigurer.html)Javadoc。 如果你更喜欢 XML 配置,那么可以使用``元素,如下例所示: ``` ``` 注意,对于前面的 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`中的例如,前面的示例也可以写成如下。

``
预定的(fixeddelay=5,timeUnit=timeUnit.seconds)
public void dosomething(){
//应该周期性运行的东西
}
```| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 如果需要固定速率执行,可以在注释中使用`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 expression](#scheduling-cron-expression)。以下示例仅在工作日运行: ``` @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`方面),其结果是每个 `@schedule’方法被调用两次。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 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`类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在`Future`上调用 `get()’之前执行其他任务。下面的示例展示了如何在返回值的方法上使用`@Async`: ``` @Async Future 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`时,使用的执行器是[启用异步支持时进行配置](#scheduling-enable-annotation-support),即“注释驱动”元素(如果你使用 XML 或`AsyncConfigurer`实现)。但是,当需要指示在执行给定方法时应该使用默认值以外的执行器时,可以使用`value`注释的`@Async`属性。下面的示例展示了如何做到这一点: ``` @Async("otherExecutor") void doSomething(String s) { // this will be run asynchronously by "otherExecutor" } ``` 在这种情况下,`"otherExecutor"`可以是 Spring 容器中任何`Executor` Bean 的名称,也可以是与任何`Executor`相关联的限定符的名称(例如,与``元素或 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`或``XML 元素来定义自定义`AsyncUncaughtExceptionHandler`。 ### 7.4.`task`名称空间 在版本 3.0 中, Spring 包括一个用于配置`TaskExecutor`和 `taskscheduler’实例的 XML 命名空间。它还提供了一种方便的方式来配置要用触发器调度的任务。 #### 7.4.1.“调度程序”元素 以下元素创建具有指定线程池大小的`ThreadPoolTaskScheduler`实例: ``` ``` 为`id`属性提供的值被用作池中线程名称的前缀。`scheduler`元素相对简单。如果不提供`pool-size`属性,则默认线程池只有一个线程。调度程序没有其他配置选项。 #### 7.4.2.`executor`元素 下面创建一个`ThreadPoolTaskExecutor`实例: ``` ``` 与[上一节](#scheduling-task-namespace-scheduler)中显示的调度程序一样,为`id`属性提供的值被用作池中线程名称的前缀。就池大小而言,`executor`元素比`scheduler`元素支持更多的配置选项。首先,`ThreadPoolTaskExecutor`的线程池本身更可配置。执行器的线程池可以为核心和最大大小提供不同的值,而不是只有一个大小。如果你只提供一个值,那么执行器有一个固定大小的线程池(核心和最大大小是相同的)。但是,`executor`元素的`pool-size`属性也接受`min-max`形式的范围。以下示例设置的最小值为 `5’,最大值为`25`: ``` ``` 在前面的配置中,还提供了一个`queue-capacity`值。线程池的配置也应该根据执行器的队列容量来考虑。有关池大小和队列容量之间关系的完整说明,请参见[“Threadpoolexecutor”](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html)的文档。其主要思想是,当提交任务时,如果当前活动线程的数量小于核心大小,则执行器首先尝试使用空闲线程。如果已达到核心大小,则将该任务添加到队列中,只要该任务的容量尚未达到。只有这样,如果队列的容量已经达到,执行器才会创建超出核心大小的新线程。如果也达到了最大大小,那么执行器将拒绝该任务。 默认情况下,队列是无界的,但这很少是所需的配置,因为如果在所有池线程都忙的时候向队列中添加了足够多的任务,则可能导致`OutOfMemoryErrors`。此外,如果队列是无界的,则最大大小完全没有影响。由于执行器总是在创建超出核心大小的新线程之前尝试队列,因此队列必须具有有限的容量,以使线程池的容量超出核心大小(这就是为什么在使用无界队列时,固定大小的线程池是唯一明智的情况)。 考虑一下上面提到的当任务被拒绝时的情况。默认情况下,当任务被拒绝时,线程池执行器抛出一个`TaskRejectedException`。然而,拒绝策略实际上是可配置的。当使用默认的拒绝策略(即`AbortPolicy`实现)时,将引发异常。对于在重负载下可以跳过某些任务的应用程序,可以配置`DiscardPolicy`或`DiscardOldestPolicy`。对于需要在重负载下控制提交任务的应用程序,另一个很好的选项是`CallerRunsPolicy`。该策略强制调用提交方法的线程运行任务本身,而不是抛出异常或丢弃任务。其思想是,这样的调用者在运行该任务时很忙,无法立即提交其他任务。因此,它提供了一种简单的方法来限制传入的负载,同时保持线程池和队列的限制。通常,这允许执行器“赶上”它正在处理的任务,从而释放队列中、池中或两者中的一些容量。你可以从`executor`元素上`rejection-policy`属性可用的值的枚举中选择这些选项中的任何一个。 下面的示例显示了一个`executor`元素,该元素具有用于指定各种行为的多个属性: ``` ``` 最后,`keep-alive`设置决定了线程在停止之前可以保持空闲的时间限制(以秒为单位)。如果池中的线程数量超过了当前的核心数量,那么在不处理任务的情况下等待了这么长的时间之后,多余的线程将被停止。时间值为零会导致多余的线程在执行任务后立即停止,而不会在任务队列中保留后续工作。下面的示例将`keep-alive`值设置为两分钟: ``` ``` #### 7.4.3.“计划任务”元素 Spring 的任务命名空间最强大的功能是支持配置要在 Spring 应用程序上下文中调度的任务。这遵循了类似于 Spring 中的其他“方法调用程序”的方法,例如由 JMS 名称空间提供的用于配置消息驱动的 POJO 的方法。基本上,`ref`属性可以指向任何 Spring 管理的对象,而`method`属性提供了要在该对象上调用的方法的名称。下面的清单展示了一个简单的示例: ``` ``` 调度程序由外部元素引用,每个单独的任务都包括其触发器元数据的配置。在前面的示例中,该元数据定义了一个具有固定延迟的周期性触发器,该延迟指示每个任务执行完成后要等待的毫秒数。另一个选项是“fixed-rate”,表示无论之前的执行需要多长时间,该方法应该运行多长时间。此外,对于`fixed-delay`和`fixed-rate`任务,你都可以指定一个“initial-delay”参数,该参数指示在第一次执行该方法之前需要等待的毫秒数。对于更多的控制,你可以提供`cron`属性来提供[cron expression](#scheduling-cron-expression)。下面的示例展示了这些其他选项: ``` ``` ### 7.5.CRON 表达式 Spring 所有 CRON 表达式都必须符合相同的格式,无论你是在[“@ 已排定的”注释](#scheduling-annotation-support-scheduled)、[“任务:排定的任务”要素](#scheduling-task-namespace-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`字符,这具有不同的含义 * 在 day-of-month 字段中,`L`表示*这个月的最后一天*。如果后接一个负偏移量(即`L-n`),则表示*这个月的最后一天*。 * 在一周一天的字段中,`L`代表*一周的最后一天*。如果前缀是数字或三个字母的名称(`dl’或`DDDL`),则表示*the last day of week (`d`or `DDD`) in the month*。 * Day-of-Month 字段可以是`nW`,它代表*the nearest weekday to day of the month `n`*。如果`n`在周六下跌,这将产生它之前的周五。如果`n`在周日下跌,这将产生之后的星期一,如果`n`是`1`并在星期六下跌,也会发生这种情况(即:`1W`代表*这个月的第一个工作日。*)。 * 如果月中日字段是`LW`,则表示*这个月的最后一个工作日*。 * 一周一天的字段可以是`d#n`(或`DDD#n`),代表*the `n`th day of week `d`(or `DDD`) in the month*。 以下是一些例子: | 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`)|一年一次(`00101*`)| | `@monthly` |每月一次(`0001* *`)| | `@weekly` |每周一次(`00* *0`)| |`@daily` (or `@midnight`) |一天一次(`00* **`),或| | `@hourly` |一小时一次,(`0* ** *`)| ### 7.6.使用 Quartz 调度器 Quartz 使用`Trigger`、`Job`、`JobDetail`对象来实现对各类作业的调度。有关 Quartz 背后的基本概念,请参见[https://www.quartz-scheduler.org/](https://www.quartz-scheduler.org/)。出于方便的目的, Spring 提供了几个类,这些类简化了基于 Spring 的应用程序中使用 Quartz 的过程。 #### 7.6.1.使用`JobDetailFactoryBean` Quartz`JobDetail`对象包含运行作业所需的所有信息。 Spring 提供了“JobDetailFactoryBean”,它为 XML 配置目的提供了 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 } } ``` 你也可以使用作业数据图中的所有附加属性。 | |通过使用`name`和`group`属性,可以分别修改作业的名称和组
。默认情况下,作业的名称与`JobDetailFactoryBean`的 Bean 名称
相匹配(在上面的示例中为 `examplejob’)。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 7.6.2.使用`MethodInvokingJobDetailFactoryBean` 通常,你只需要在特定对象上调用一个方法。通过使用“MethodinkingJobDetailFactoryBean”,你可以做到这一点,如下例所示: ``` ``` 前面的示例导致`doIt`方法在 `ExampleBusinessObject’方法上被调用,如下例所示: ``` public class ExampleBusinessObject { // properties and collaborators public void doIt() { // do the actual work } } ``` ``` ``` 通过使用`MethodInvokingJobDetailFactoryBean`,你不需要创建仅调用方法的单行作业。你只需要创建实际的业务对象并连接详细的对象。 默认情况下,Quartz 作业是无状态的,这导致了作业相互干扰的可能性。如果为同一个`JobDetail`指定两个触发器,则可能在第一个作业完成之前,第二个作业就开始了。如果“JobDetail”类实现`Stateful`接口,则不会发生这种情况。第二项工作在第一项工作完成之前不会开始。要使“MethodinkingJobDetailFactoryBean”生成的作业非并发,请将标志设置为“false”,如下例所示: ``` ``` | |默认情况下,作业将以并发方式运行。| |---|--------------------------------------------------| #### 7.6.3.使用触发器和`SchedulerFactoryBean`连接作业 我们创造了工作细节和工作岗位。 Bean 我们还介绍了使你能够在特定对象上调用方法的便利性。当然,我们仍然需要自己安排工作。这是通过使用触发器和`SchedulerFactoryBean`来完成的。Quartz 中有几个触发器可用, Spring 提供了两个 Quartz`FactoryBean`实现,它们具有方便的默认值:`CronTriggerFactoryBean`和 `SimpleTriggerFactoryBean’。 需要对触发器进行计划。 Spring 提供了一个`SchedulerFactoryBean`,它公开了要设置为属性的触发器。`SchedulerFactoryBean`使用这些触发器来调度实际的作业。 下面的列表使用了`SimpleTriggerFactoryBean`和`CronTriggerFactoryBean`: ``` ``` 前面的示例设置了两个触发器,一个是每 50 秒运行一次,启动延迟 10 秒,另一个是每天早上 6 点运行。要最终确定所有内容,我们需要设置“SchedulerFactoryBean”,如下例所示: ``` ``` 对于`SchedulerFactoryBean`,有更多的属性可用,例如作业详细信息使用的日历、用来自定义 Quartz 的属性,以及 Spring 提供的 JDBC 数据源。有关更多信息,请参见[“SchedulerFactoryBean”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/scheduling/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 应用程序透明地添加缓存的支持。与[transaction](data-access.html#transaction)支持类似,缓存抽象允许一致地使用各种缓存解决方案,并且对代码的影响最小。 在 Spring Framework4.1 中,缓存抽象得到了显著扩展,支持[JSR-107 注释](#cache-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.cache”和`org.springframework.cache.CacheManager`接口实现的。 Spring 提供了该抽象的[几个实现](#cache-store-configuration):基于 JDK`java.util.concurrent.ConcurrentMap`的缓存、[Ehcache 2.x](https://www.ehcache.org/)、Gemfire 缓存、[Caffeine](https://github.com/ben-manes/caffeine/wiki),以及与 JSR-107 兼容的缓存(例如 EHCache3.x)。有关插入其他缓存存储和提供程序的更多信息,请参见[插入不同的后端缓存](#cache-plug)。 | |对于多线程和
多进程环境,缓存抽象没有特殊的处理,因为这些特性由缓存实现处理。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| 如果你有一个多进程环境(即部署在多个节点上的应用程序),则需要相应地配置你的缓存提供程序。根据你的用例,在几个节点上复制相同的数据就足够了。但是,如果在应用程序运行过程中更改了数据,则可能需要启用其他传播机制。 缓存特定的项目直接等同于在编程缓存交互中发现的典型的“如果没有找到,就继续进行并最终放置”的代码块。没有应用锁,并且多个线程可能会尝试并发加载相同的项。这同样适用于驱逐。如果多个线程试图同时更新或删除数据,则可能会使用过时的数据。某些缓存提供商在该领域提供了高级功能。有关更多详细信息,请参见缓存提供程序的文档。 要使用缓存抽象,你需要注意两个方面: * 缓存声明:确定需要缓存的方法及其策略。 * 缓存配置:存储数据并从中读取数据的备份缓存。 ### 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](https://jira.spring.io/browse/SPR-10237))。新的`SimpleKeyGenerator`在这种情况下使用复合键。

如果你想继续使用以前的键策略,可以配置不受欢迎的 `org.springframework.cache.interceptor.defaultkeygenerator’类或创建一个自定义的
基于散列的`KeyGenerator`实现。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 自定义密钥生成声明 由于缓存是通用的,目标方法很可能具有各种签名,而这些签名不能很容易地映射到缓存结构的顶部。当目标方法有多个参数时,这一点往往变得很明显,其中只有一些参数适合缓存(而其余的参数仅由方法逻辑使用)。考虑以下示例: ``` @Cacheable("books") public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) ``` 乍一看,虽然两个`boolean`参数会影响找到这本书的方式,但它们对缓存没有用。此外,如果这两个因素中只有一个是重要的,而另一个不是重要的,那该怎么办? 对于这种情况,`@Cacheable`注释允许你指定如何通过其`key`属性生成密钥。你可以使用[SpEL](core.html#expressions)来选择感兴趣的参数(或它们的嵌套属性),执行操作,甚至调用任意方法,而无需编写任何代码或实现任何接口。这是在[默认生成器](#cache-annotations-cacheable-default-key)上推荐的方法,因为随着代码库的增长,签名中的方法往往会有很大的不同。虽然默认策略可能对某些方法有效,但它很少对所有方法有效。 以下示例使用了各种 SPEL 声明(如果你不熟悉 SPEL,请帮自己一个忙,并阅读[Spring Expression Language](core.html#expressions)): ``` @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) ``` | |`key`和`keyGenerator`参数是互斥的,指定这两个参数的操作
会导致异常。| |---|--------------------------------------------------------------------------------------------------------------------------------| ##### 默认缓存分辨率 缓存抽象使用一个简单的`CacheResolver`,该抽象使用配置的“CacheManager”检索在操作级别定义的缓存。 要提供不同的默认缓存解析器,你需要实现 `org.SpringFramework.cache.Interceptor.CacheResolver’接口。 ##### 自定义缓存分辨率 默认的缓存分辨率非常适合使用单个`CacheManager`且没有复杂的缓存分辨率要求的应用程序。 对于使用多个缓存管理器的应用程序,可以设置用于每个操作的“CacheManager”,如下例所示: ``` @Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1) public Book findBook(ISBN isbn) {...} ``` |**1**|指定`anotherCacheManager`。| |-----|---------------------------------| 你还可以完全以类似于替换[key generation](#cache-annotations-cacheable-key)的方式替换`CacheResolver`。对每个缓存操作都请求解析,让实现根据运行时参数实际解析要使用的缓存。下面的示例展示了如何指定`CacheResolver`: ``` @Cacheable(cacheResolver="runtimeCacheResolver") (1) public Book findBook(ISBN isbn) {...} ``` |**1**|指定`CacheResolver`。| |-----|-------------------------------| | |自 Spring 4.1 起,`value`属性的缓存注释不再是
强制的,因为该特定信息可以由`CacheResolver`提供,而与注释的内容无关。
`key`与`keyGenerator`类似,`cacheManager`和`cacheResolver`的参数是互斥的,并且指定
的操作会导致异常,因为自定义的`CacheManager`被 cachesolver 实现忽略。这可能不是你所期望的。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 同步缓存 在多线程环境中,某些操作可能会为相同的参数并发调用(通常是在启动时)。默认情况下,缓存抽象不会锁定任何内容,并且相同的值可能会被多次计算,从而破坏了缓存的目的。 对于这些特殊情况,可以使用`sync`属性来指示底层缓存提供程序在计算值时锁定缓存条目。结果,只有一个线程在忙着计算值,而其他线程则被阻塞,直到在缓存中更新条目。下面的示例展示了如何使用`sync`属性: ``` @Cacheable(cacheNames="foos", sync=true) (1) public Foo executeExpensiveOperation(String id) {...} ``` |**1**|使用`sync`属性。| |-----|---------------------------| | |这是一个可选的特性,并且你最喜欢的缓存库可能不支持它。
所有由核心框架提供的`CacheManager`实现都支持它。有关更多详细信息,请参见缓存提供程序的
文档。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 条件缓存 有时,一个方法可能不适合始终进行缓存(例如,它可能取决于给定的参数)。缓存注释通过“条件”参数支持这样的用例,该参数接受一个`SpEL`表达式,该表达式被求值为`true`或`false`。如果`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 findBook(String name) ``` 注意,`#result`仍然是指`Book`,而不是`Optional`。因为它可能是“null”,所以我们使用 spel 的[安全导航操作员](core.html#expressions-operator-safe-navigation)。 ##### 可用的缓存 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`表达式(当`beforeInvocation`为`false`时)中可用。对于受支持的包装器(例如“可选的”),`#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`上的定制集。因此,这为每个缓存操作提供了三个级别的自定义: * 全局配置,可用于`CacheManager`,`KeyGenerator`。 * 在类级别上,使用`@CacheConfig`。 * 在操作层面。 #### 8.2.6.启用缓存注释 重要的是要注意,即使声明缓存注释并不会自动触发它们的动作--就像 Spring 中的许多事情一样,该功能必须以声明方式启用(这意味着,如果你怀疑这是缓存造成的,你可以通过只删除一个配置行而不是代码中的所有注释来禁用它)。 要启用缓存注释,请将注释`@EnableCaching`添加到一个 `@configuration’类中: ``` @Configuration @EnableCaching public class AppConfig { } ``` 或者,对于 XML 配置,你可以使用`cache:annotation-driven`元素: ``` ``` `cache:annotation-driven`元素和`@EnableCaching`注释都允许你指定各种选项,这些选项会影响通过 AOP 将缓存行为添加到应用程序的方式。该配置有意地与[@transactional`](data-access.html#tx-annotation-driven-settings)的配置相似。 | |处理缓存注释的默认通知模式是`proxy`,它允许
仅通过代理拦截调用。同一类
中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,可以考虑将
转换为`aspectj`模式,并结合编译时或加载时编织。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |有关实现`CachingConfigurer`所需的
的高级定制(使用 Java 配置)的更多详细信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html)。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | XML Attribute | Annotation Attribute | Default |说明| |--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `cache-manager` |N/A (see the [`CachingConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html) javadoc)| `cacheManager` |要使用的缓存管理器的名称。默认的`CacheResolver`在
使用此缓存管理器的场景后面初始化(如果未设置`cacheManager`)。要了解更多
对缓存解析度的细粒度管理,请考虑设置“cache-resolver”
属性。| | `cache-resolver` |N/A (see the [`CachingConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html) javadoc)|A `SimpleCacheResolver` using the configured `cacheManager`.|Bean 用于解析备份缓存的 CacheResolver 的名称。
此属性不是必需的,只需要指定为
“cache-manager”属性的替代项。| | `key-generator` |N/A (see the [`CachingConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html) javadoc)| `SimpleKeyGenerator` |要使用的自定义密钥生成器的名称。| | `error-handler` |N/A (see the [`CachingConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html) javadoc)| `SimpleCacheErrorHandler` |要使用的自定义缓存错误处理程序的名称。默认情况下,在
缓存相关操作期间抛出的任何异常都会在客户端被抛回。| | `mode` | `mode` | `proxy` |默认模式处理要通过使用 Spring 的 AOP
框架(遵循代理语义,如前面讨论的那样,应用于仅通过代理进入的方法调用
)来代理的注释 bean。替代模式使用 Spring 的 AspectJ 缓存方面来编织
受影响的类,修改目标类字节
代码,以应用于任何类型的方法调用。AspectJ 编织需要在 Classpath 中`spring-aspects.jar`以及启用加载时编织(或编译时编织)。(有关如何设置
加载时编织的详细信息,请参见[Spring configuration](core.html#aop-aj-ltw-spring)。| |`proxy-target-class`| `proxyTargetClass` | `false` |仅适用于代理模式。控制为使用`@Cacheable`或`@CacheEvict`注释的
类创建的缓存代理类型。如果“proxy-target-class”属性设置为`true`,则创建基于类的代理。
如果`proxy-target-class`是`false`,或者如果省略了该属性,则创建标准的基于接口的代理。(有关不同代理类型的详细检查,请参见[代理机制](core.html#aop-proxying)。| | `order` | `order` | Ordered.LOWEST\_PRECEDENCE |定义应用于带“@cacheable”或`@CacheEvict`注释的 bean 的缓存通知的顺序。(有关
排序 AOP 通知的规则的更多信息,请参见[Advice Ordering](core.html#aop-ataspectj-advice-ordering)。)
没有指定的排序意味着 AOP 子系统确定通知的顺序。| | |``仅在与其定义相同的应用程序上下文中的 bean 上查找`@Cacheable/@CachePut/@CacheEvict/@Caching`。这意味着,
如果将``放在`WebApplicationContext`中的 `dispatcherservlet’,它只会检查控制器中的 bean,而不是你的服务。
有关更多信息,请参见[the MVC section](web.html#mvc-servlet)。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 方法可见性和缓存注释 当你使用代理时,你应该只将缓存注释应用于具有公共可见性的方法。如果使用这些注释对受保护的、私有的或包可见的方法进行注释,则不会产生错误,但是注释的方法不显示已配置的缓存设置。如果需要对非公共方法进行注释,请考虑使用 AspectJ(请参阅本节的其余部分),因为它会更改字节码本身。 | |Spring 建议你只使用`@Cache*`注释来注释具体的类(和具体的
类的方法),而不是注释接口。
你当然可以在接口(或接口
方法)上放置`@Cache*`注释,但这仅在使用代理模式(`mode=“proxy”`)的情况下才有效。如果使用
基于编织的方面(`mode=“AspectJ”`),则在
接口级别声明中,编织基础设施不会识别缓存设置。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |在代理模式(默认)中,只有通过
代理进入的外部方法调用才会被拦截。这意味着,自我调用(实际上,
目标对象中的方法调用了目标对象的另一个方法)在运行时不会导致实际的
缓存,即使调用的方法被标记为`@Cacheable`。在这种情况下,使用`aspectj`模式考虑
。此外,代理必须完全初始化为
提供预期的行为,因此你不应该在
初始化代码(即`@PostConstruct`)中依赖此功能。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 8.2.7.使用自定义注释 自定义注释和 AspectJ 该功能仅适用于基于代理的方法,但可以通过使用 AspectJ 进行一些额外的工作来启用。 `spring-aspects`模块仅为标准注释定义了一个方面。如果你已经定义了自己的注释,那么还需要为这些注释定义一个方面。检查`AnnotationCacheAspect`以获取示例。 缓存抽象允许你使用自己的注释来识别触发缓存填充或驱逐的方法。作为一种模板机制,这非常方便,因为它消除了重复缓存注释声明的需要,如果指定了键或条件,或者你的代码库中不允许外国导入,这一点尤其有用。与[stereotype](core.html#beans-stereotype-annotations)注释的其余部分类似,你可以使用`@Cacheable`、`@CachePut`、`@CacheEvict`和`@CacheConfig`作为[元注释](core.html#beans-meta-annotations)(即可以注释其他注释的注释)。在下面的示例中,我们用自己的自定义注释替换了一个常见的“@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](#cache-annotation-enable)中提到的,需要启用注释驱动的行为。 ### 注解 从版本 4.1 开始, Spring 的缓存抽象完全支持 JCache 标准(JSR-107)注释:`@CacheResult`,`@CachePut`,`@CacheRemove`,和`@CacheRemoveAll`,以及`@CacheDefaults`,`@CacheKey`伙伴关系。即使不将缓存存储迁移到 JSR-107,也可以使用这些注释。内部实现使用 Spring 的缓存抽象,并提供符合规范的缺省“CacheResolver”和实现。换句话说,如果你已经在使用 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 的[自定义密钥生成声明](#cache-annotations-cacheable-key)。例如,以下是相同的操作,一个使用 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 和 ` Spring-上下文支持 ` 模块,则`@EnableCaching`和`cache:annotation-driven`XML 元素都会自动启用 JCache 支持。 | |根据你的用例,选择基本上是你的。你甚至可以混合和
匹配服务,方法是在某些服务上使用 JSR-107API,并在
其他服务上使用 Spring 自己的注释。但是,如果这些服务影响相同的缓存,则应该使用一致的
和相同的密钥生成实现。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 8.4.声明式基于 XML 的缓存 如果注释不是一种选择(可能是由于无法访问源代码或没有外部代码),则可以使用 XML 进行声明式缓存。因此,你可以在外部指定目标方法和缓存指令,而不是注释用于缓存的方法(类似于声明性事务管理[advice](data-access.html#transaction-declarative-first-example))。上一节的示例可以转换为以下示例: ``` ``` 在前面的配置中,`bookService`是可缓存的。要应用的缓存语义封装在`cache:advice`定义中,这导致`findBooks`方法用于将数据放入缓存,而`loadBooks`方法用于驱逐数据。这两个定义都针对`books`缓存。 `aop:config`定义通过使用 AspectJ PointCut 表达式将缓存通知应用到程序中的适当点(更多信息可在[Aspect Oriented Programming with Spring](core.html#aop)中获得)。在前面的示例中,将考虑来自`BookService`的所有方法,并将缓存通知应用于它们。 声明式 XML 缓存支持所有基于注释的模型,因此在两者之间移动应该非常容易。此外,两者都可以在同一个应用程序中使用。基于 XML 的方法不会触及目标代码。然而,它本质上更加冗长。当处理具有针对缓存的重载方法的类时,确定正确的方法确实需要额外的努力,因为`method`参数不是一个好的鉴别器。在这些情况下,你可以使用 AspectJ 切入点来挑选目标方法并应用适当的缓存功能。然而,通过 XML,应用包或组或接口范围的缓存(同样由于 AspectJ 切入点)和创建模板类定义(就像我们在前面的示例中所做的那样,通过`cache:definitions``cache` 属性定义目标缓存)更容易。 ### 8.5.配置缓存存储 缓存抽象提供了几个存储集成选项。要使用它们,你需要声明一个适当的`CacheManager`(一个控制和管理`Cache`实例的实体,该实例可用于检索这些实例以进行存储)。 #### 8.5.1.基于 jdk`ConcurrentMap`的缓存 基于 JDK 的`Cache`实现位于 `org.springframework.cache.concurrent`package 下。它允许你使用`ConcurrentHashMap`作为备份`Cache`存储。下面的示例展示了如何配置两个缓存: ``` ``` 前面的代码片段使用`SimpleCacheManager`为两个名为`ConcurrentMapCache`和`books`的嵌套实例创建`CacheManager`。请注意,名称是直接为每个缓存配置的。 由于缓存是由应用程序创建的,因此它与其生命周期绑定在一起,这使得它适合于基本的用例、测试或简单的应用程序。该缓存可以很好地扩展并且非常快,但是它不提供任何管理、持久性功能或驱逐契约。 #### 8.5.2.基于 eHcache 的缓存 | |Ehcache3.x 完全兼容 JSR-107,不需要专门的支持。| |---|-----------------------------------------------------------------------------------| EhCache2.x 实现位于`org.springframework.cache.ehcache`包中。同样,要使用它,你需要声明适当的`CacheManager`。下面的示例展示了如何做到这一点: ``` ``` 此设置引导 Spring IoC 中的 EHCache 库(通过`ehcache` Bean),然后将其连接到专用的`CacheManager`实现中。请注意,整个 EHCache 特定的配置是从`ehcache.xml`读取的。 #### 8.5.3.咖啡因缓存 咖啡因是对芭乐缓存的 Java8 重写,其实现位于“org.springframework.cache.caffeine”包中,并提供了对咖啡因的几个功能的访问。 下面的示例配置了一个`CacheManager`,它会按需创建缓存: ``` ``` 你还可以提供要显式使用的缓存。在这种情况下,只有那些是由经理提供的。下面的示例展示了如何做到这一点: ``` default books ``` 咖啡因`CacheManager`还支持自定义`Caffeine`和`CacheLoader`。有关这些问题的更多信息,请参见[咖啡因文档](https://github.com/ben-manes/caffeine/wiki)。 #### 8.5.4.基于 Gemfire 的高速缓存 Gemfire 是一个面向内存、磁盘支持、弹性可伸缩、持续可用、活动的(具有内置的基于模式的订阅通知)、全局复制的数据库,并提供功能齐全的边缘缓存。有关如何使用 Gemfire 作为`CacheManager`(以及更多)的更多信息,请参见[Spring Data GemFire reference documentation](https://docs.spring.io/spring-gemfire/docs/current/reference/html/)。 #### 8.5.5.JSR-107 高速缓存 Spring 的缓存抽象还可以使用兼容 JSR-107 的缓存。JCache 实现位于`org.springframework.cache.jcache`包中。 同样,要使用它,你需要声明适当的`CacheManager`。下面的示例展示了如何做到这一点: ``` ``` #### 8.5.6.处理没有后台存储的缓存 有时,在切换环境或进行测试时,你可能会有缓存声明,而没有配置实际的备份缓存。由于这是一个无效的配置,在运行时会引发一个异常,因为缓存基础设施无法找到合适的存储。在这种情况下,你可以连接到一个不执行缓存的简单虚拟缓存,而不是删除缓存声明(这可能会很乏味)——也就是说,它强制每次调用缓存的方法。下面的示例展示了如何做到这一点: ``` ``` 在前面的链中的`CompositeCacheManager`多个`CacheManager`实例,并通过`fallbackToNoOpCache`标志,为所有未由配置的缓存管理器处理的定义添加了一个无操作缓存。也就是说,在`jdkCache`或`gemfireCache`(在示例的前面进行了配置)中找不到的每个缓存定义都由非 OP 缓存处理,该缓存不存储任何信息,导致每次都调用目标方法。 ### 8.6.插入不同的后端缓存 显然,有很多缓存产品可以用作后台商店。对于那些不支持 JSR-107 的,你需要提供`CacheManager`和 `cache’实现。这听起来可能比实际情况更难,因为在实践中,类往往是简单的[adapters](https://en.wikipedia.org/wiki/Adapter_pattern),它将缓存抽象框架映射到存储 API 之上,就像`ehcache`类那样。大多数`CacheManager`类可以使用 `org.springframework.cache.support’包中的类(例如`AbstractCacheManager`,它负责锅炉板代码,只留下实际的映射要完成)。 ### 8.7.我如何设置 TTL/TTI/驱逐策略/XXX 功能? 直接通过你的缓存提供程序。缓存抽象是一个抽象,而不是一个缓存实现。你使用的解决方案可能支持其他解决方案不支持的各种数据策略和不同的拓扑(例如,JDK`ConcurrentHashMap`——在缓存抽象中暴露这一点将是无用的,因为没有支持)。这样的功能应该直接通过后台缓存(在配置时)或通过其本地 API 进行控制。 ## 9. 附录 ### 9.1.XML 模式 附录的这一部分列出了与集成技术相关的 XML 模式。 #### 9.1.1.`jee`模式 `jee`元素处理与 Java EE(Java Enterprise 版本)配置有关的问题,例如查找 JNDI 对象和定义 EJB 引用。 要使用`jee`模式中的元素,你需要在 Spring XML 配置文件的顶部有以下序言。以下代码片段中的文本引用了正确的模式,因此`jee`名称空间中的元素对你是可用的: ``` ``` ##### \(简单) 下面的示例展示了如何使用 JNDI 在没有`jee`模式的情况下查找数据源: ``` ``` 下面的示例展示了如何使用 JNDI 查找带有`jee`模式的数据源: ``` ``` ##### ``(带有单个 JNDI 环境设置) 下面的示例展示了如何使用 JNDI 查找不带“JEE”的环境变量: ``` pong ``` 下面的示例展示了如何使用 JNDI 查找带有`jee`的环境变量: ``` ping=pong ``` ##### ``(具有多个 JNDI 环境设置) 下面的示例展示了如何使用 JNDI 在不`jee`的情况下查找多个环境变量: ``` song pong ``` 下面的示例展示了如何使用 JNDI 用“JEE”查找多个环境变量: ``` sing=song ping=pong ``` ##### ``(复合) 下面的示例展示了如何使用 JNDI 在没有`jee`的情况下查找数据源和许多不同的属性: ``` ``` 下面的示例展示了如何使用 JNDI 用`jee`查找数据源和许多不同的属性: ``` ``` ##### ``(简单) Bean ``元素配置了对本地 EJB 无状态会话的引用。 下面的示例展示了如何在没有`jee`的情况下配置对本地 EJB 无状态会话的引用 Bean: ``` ``` 下面的示例展示了如何使用`jee`配置对本地 EJB 无状态会话 Bean 的引用: ``` ``` ##### ``(复数) ``元素配置了对本地 EJB 无状态会话的引用 Bean。 下面的示例展示了如何配置对本地 EJB 无状态会话 Bean 的引用和一些不带`jee`的属性: ``` ``` 下面的示例展示了如何配置对本地 EJB 无状态会话 Bean 的引用,以及使用`jee`的许多属性: ``` ``` ##### \ ``元素配置了对`remote`EJB 无状态会话 Bean 的引用。 下面的示例展示了如何在没有`jee`的情况下配置对远程 EJB 无状态会话 Bean 的引用: ``` ``` 下面的示例展示了如何使用`jee`配置对远程 EJB 无状态会话 Bean 的引用: ``` ``` #### 9.1.2.`jms`模式 `jms`元素处理与 JMS 相关的 bean 的配置,例如 Spring 的[消息监听器容器](#jms-mdp)。这些要素在[JMS chapter](#jms)题为[JMS 名称空间支持](#jms-namespace)的一节中有详细说明。有关此支持和`jms`元素本身的详细信息,请参见该章。 为了完整起见,要使用`jms`模式中的元素,你需要在 Spring XML 配置文件的顶部有以下序言。以下代码片段中的文本引用了正确的模式,因此`jms`名称空间中的元素对你是可用的: ``` ``` #### 9.1.3.使用`` 这个元素在[配置基于注释的 MBean 导出](#jmx-context-mbeanexport)中有详细说明。 #### 9.1.4.`cache`模式 你可以使用`cache`元素来支持 Spring 的`@CacheEvict`、`@CachePut`和`@Caching`注释。它还支持声明式的基于 XML 的缓存。详见[启用缓存注释](#cache-annotation-enable)和[声明式基于 XML 的缓存](#cache-declarative-xml)。 要使用`cache`模式中的元素,你需要在 Spring XML 配置文件的顶部有以下序言。以下代码片段中的文本引用了正确的模式,因此`cache`名称空间中的元素对你是可用的: ``` ```