本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

参考文档的这一部分涵盖了Spring Framework与一些技术的集成(整合)。

1. REST 客户端

Spring框架为调用REST端点提供了以下选择:

  • WebClient - 非阻塞、响应式客户端和 fluent API。

  • RestTemplate - 带有模板方法API的同步客户端。

  • HTTP 接口 - 注解式接口,并生成动态代理实现。

1.1. WebClient

WebClient 是一个非阻塞的、响应式的客户端,用于执行HTTP请求。它在5.0中引入,提供了 RestTemplate 的替代方案,支持同步、异步和流式场景。

WebClient 支持以下特性:

  • 非阻塞 I/O。

  • Reactive Streams 背压。

  • 以较少的硬件资源实现高并发性。

  • 函数式、fluent API,利用了Java 8 lambdas的优势。

  • 同步和异步互动。

  • 服务器的流式上传和下载。

详情见 WebClient

1.2. RestTemplate

RestTemplate 提供了一个比HTTP客户端库更高层次的API。它使得在一行中调用REST端点变得容易。它暴露了以下几组重载方法:

RestTemplate 正处于维护模式,只接受小的修改和错误的请求。请考虑使用 WebClient 来代替。
Table 1. RestTemplate methods
方法组 说明

getForObject

通过 GET 检索一个表示结果。

getForEntity

通过使用GET检索一个 ResponseEntity(即 status、header 和 body)。

headForHeaders

通过使用 HEAD 检索一个资源的所有 header。

postForLocation

通过使用 POST 创建一个新的资源,并从响应中返回 Location header。

postForObject

通过使用POST创建一个新资源,并从响应中返回表示。

postForEntity

通过使用POST创建一个新资源,并从响应中返回表示。

put

通过使用PUT创建或更新一个资源。

patchForObject

通过使用 PATCH 更新一个资源,并从响应中返回表示。请注意,JDK的 HttpURLConnection 不支持 PATCH,但 Apache 的 HttpComponents 和其他的支持。

delete

通过使用 DELETE 删除指定URI上的资源。

optionsForAllow

通过使用 ALLOW 为资源检索允许的HTTP方法。

exchange

前面的方法的更通用(和更少的意见)版本,在需要时提供额外的灵活性。它接受一个 RequestEntity(包括HTTP方法、URL、header 和 body 作为输入)并返回一个 ResponseEntity

这些方法允许使用 ParameterizedTypeReference 而不是 Class 来指定一个具有泛型的响应类型。

execute

执行请求的最通用方式,通过回调接口对请求准备和响应提取进行完全控制。

1.2.1. 初始化

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

  • Apache HttpComponents

  • Netty

  • OkHttp

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

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

每个 ClientHttpRequestFactory 都公开了底层HTTP客户端库的特定配置选项—​例如,对于凭证、连接池和其他细节。

请注意,当访问代表错误(如401)的响应状态时,用于HTTP请求的 java.net 实现会引发一个异常。如果这是一个问题,请切换到另一个HTTP客户端库。
RestTemplate 可以被用于观测,以产生指标和跟踪。参见 RestTemplate 可观察性支持 部分。
URI

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

下面的例子使用了一个 String 变量的参数:

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

下面的例子使用一个 Map<String, String>

Map<String, String> vars = Collections.singletonMap("hotel", "42");

String result = restTemplate.getForObject(
        "https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);

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

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

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

你可以使用 RestTemplateuriTemplateHandler 属性来定制URI的编码方式。或者,你可以准备一个 java.net.URI,并把它传入接受 URIRestTemplate 方法之一。

关于URI的工作和编码的更多细节,请参见 URI链接

Header

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

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

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

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

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

你可以通过许多返回 ResponseEntityRestTemplate 方法变体获得响应头信息。

1.2.2. Body

HttpMessageConverter 的帮助下,传入 RestTemplate 方法和从 RestTemplate 方法返回的对象被转换为原始内容。

在POST中,一个输入对象被序列化到请求体中,如下面的例子所示:

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

你不需要明确地设置请求的 Content-Type 头。在大多数情况下,你可以根据源对象类型找到一个兼容的消息转换器(message converter),所选择的消息转换器会相应地设置 content type。如果有必要,你可以使用 exchange 方法来明确地提供 Content-Type 的请求头,而这又会影响到选择何种消息转换器。

在一个GET中,响应的 body 被反序列化为一个输出 Object,如下例所示:

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

请求的 Accept 标头不需要明确设置。在大多数情况下,可以根据预期的响应类型找到一个兼容的消息转换器,然后帮助填充 Accept 头。如果有必要,你可以使用 exchange 方法来明确提供 Accept 头。

默认情况下,RestTemplate 注册了所有内置的 消息转换器(message converter),这取决于 classpath 检查,有助于确定有哪些可选的 converter 库存在。你也可以明确地设置要使用的消息转换器。

消息转换

spring-web 模块包含 HttpMessageConverter contract,用于通过 InputStreamOutputStream 读写 HTTP 请求和响应的 body。HttpMessageConverter 实例在客户端(例如,在 RestTemplate 中)和服务器端(例如,在Spring MVC REST controller 中)使用。

框架中提供了主要 type(MIME)type 的具体实现,默认情况下,在客户端与 RestTemplate 注册,在服务器端与 RequestMappingHandlerAdapter 注册(参见 配置消息转换器)。

HttpMessageConverter 的实现将在下面几节中描述。对于所有的转换器,都使用默认的 media type,但是你可以通过设置 supportedMediaTypes bean 属性来覆盖它。下表描述了每个实现:

Table 2. HttpMessageConverter 实现
MessageConverter 说明

StringHttpMessageConverter

一个 HttpMessageConverter 实现,可以从HTTP请求和响应中读写 String 实例。默认情况下,这个转换器支持所有的文本媒体类型(text/*),并以 text/plainContent-Type 进行写入。

FormHttpMessageConverter

一个 HttpMessageConverter 实现,可以从HTTP请求和响应中读写表单数据。默认情况下,这个转换器读取和写入 application/x-www-form-urlencoded 媒体类型。表单数据从一个 MultiValueMap<String, String> 中读取并写入。该转换器还可以写入(但不读取)从 MultiValueMap<String, Object> 中读取的 multipart 数据。默认情况下,支持 multipart/form-data。从Spring Framework 5.2开始,可以支持额外的 multipart subtype 来写入表单数据。请查阅 FormHttpMessageConverter 的 javadoc 以了解更多细节。

ByteArrayHttpMessageConverter

一个 HttpMessageConverter 实现,可以从HTTP请求和响应中读写字节数。默认情况下,这个转换器支持所有的 media type(*/*),并以 application/octet-streamContent-Type 进行写入。你可以通过设置 supportedMediaTypes 属性和覆盖 getContentType(byte[]) 来重写这一点。

MarshallingHttpMessageConverter

一个 HttpMessageConverter 实现,通过使用 org.springframework.oxm 包中 Spring 的 MarshallerUnmarshaller 抽象,可以读写XML。这个转换器在使用前需要一个 MarshallerUnmarshaller。你可以通过构造函数或Bean属性注入这些东西。默认情况下,这个转换器支持 text/xmlapplication/xml

MappingJackson2HttpMessageConverter

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

MappingJackson2XmlHttpMessageConverter

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

SourceHttpMessageConverter

一个 HttpMessageConverter 实现,可以从HTTP请求和响应中读写 javax.xml.transform.Source。只有 DOMSourceSAXSourceStreamSource 被支持。默认情况下,这个转换器支持 text/xmlapplication/xml

BufferedImageHttpMessageConverter

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

1.2.3. Jackson JSON 视图

你可以指定一个 Jackson JSON 视图 来只序列化对象属性的一个子集,如下面的例子所示:

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

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

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

1.2.4. Multipart

为了发送 multipart 数据,你需要提供一个 MultiValueMap<String, Object>,其值可以是一个用于 part 内容的 Object,一个用于文件 part 的 Resource,或者一个用于带有 header 的 part 内容的 HttpEntity。比如说:

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

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

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

在大多数情况下,你不需要为每个 part 指定 Content-Type。content type 是根据为序列化它而选择的 HttpMessageConverter 自动确定的,或者在 Resource 的情况下,根据文件扩展名确定。如果有必要,你可以用一个 HttpEntity wrapper 明确地提供 MediaType

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

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

如果 MultiValueMap 至少包含一个非 String 值,Content-Type 就被 FormHttpMessageConverter 设置为 multipart/form-data。如果 MultiValueMapString 值,Content-Type 被默认为 application/x-www-form-urlencoded。如果有必要,也可以明确设置 Content-Type

1.3. HTTP 接口

Spring框架允许你将HTTP服务定义为一个Java接口,并为 HTTP exchange 提供注解方法。然后你可以生成一个实现该接口并执行 exchange 的代理。这有助于简化HTTP远程访问,因为远程访问通常涉及到一个 facade,该 facade 包装了使用底层HTTP客户端的细节。

一,声明一个带有 @HttpExchange 方法的接口:

interface RepositoryService {

    @GetExchange("/repos/{owner}/{repo}")
    Repository getRepository(@PathVariable String owner, @PathVariable String repo);

    // more HTTP exchange methods...

}

二,创建一个代理,执行所声明的 HTTP exchange:

WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();

RepositoryService service = factory.createClient(RepositoryService.class);

在类型层面上支持 @HttpExchange,它适用于所有方法:

@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
interface RepositoryService {

    @GetExchange
    Repository getRepository(@PathVariable String owner, @PathVariable String repo);

    @PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    void updateRepository(@PathVariable String owner, @PathVariable String repo,
            @RequestParam String name, @RequestParam String description, @RequestParam String homepage);

}

1.3.1. 方法参数

注解式 HTTP exchange 方法支持灵活的方法签名,有以下方法参数:

方法参数 说明

URI

动态设置请求的URL,覆盖注解的 url 属性。

HttpMethod

动态地设置请求的HTTP方法,覆盖注解的 method 属性

@RequestHeader

添加一个请求头或多个头。参数可以是一个 Map<String, ?>MultiValueMap<String, ?> 的多个头,一个 Collection<?>,或一个单独的值。对于非 String 值,支持类型转换。

@PathVariable

添加一个变量,用于在请求URL中扩展一个占位符。参数可以是一个带有多个变量的 Map<String, ?>,也可以是一个单独的值。对于非 String 值,支持类型转换。

@RequestBody

提供请求的 body,可以是要被序列化的对象,也可以是Reactive Streams Publisher,如 MonoFlux,或通过配置的 ReactiveAdapterRegistry 支持的任何其他异步类型。

@RequestParam

添加一个请求参数或多个参数。参数可以是一个 Map<String, ?>MultiValueMap<String, ?> 的多个参数,一个 Collection<?> ,或一个单独的值。对于非 String 值,支持类型转换。

"content-type" 被设置为 "application/x-www-form-urlencoded" 时,请求参数被编码在请求体中。否则,它们被添加为URL查询参数。

@RequestPart

添加一个 request part,它可以是一个 String(表单字段)、Resource(文件 part)、对象(要编码的实体,如JSON)、HttpEntity(part 内容和 header)、Spring Part 或上述任何一个的 Reactive Streams Publisher

@CookieValue

添加一个或多个cookie。参数可以是一个 Map<String, ?>MultiValueMap<String, ?> 的多个cookie,一个 Collection<?>,或一个单独的值。对于非 String 值,支持类型转换。

1.3.2. 返回值

注解式 HTTP exchange 方法支持以下返回值:

方法返回值 说明

void, Mono<Void>

执行给定的请求,并释放响应内容,如果有的话。

HttpHeaders, Mono<HttpHeaders>

执行给定的请求,释放响应内容(如果有),并返回响应头信息。

<T>, Mono<T>

执行给定的请求并将响应内容解码为声明的返回类型。

<T>, Flux<T>

执行给定的请求,并将响应内容解码为声明的元素类型的流。

ResponseEntity<Void>, Mono<ResponseEntity<Void>>

执行给定的请求,并释放响应内容(如果有的话),并返回一个带有状态和头信息的 ResponseEntity

ResponseEntity<T>, Mono<ResponseEntity<T>>

执行给定的请求,将响应内容解码为声明的返回类型,并返回一个带有状态、头信息和解码后的 body 的 ResponseEntity

Mono<ResponseEntity<Flux<T>>

执行给定的请求,将响应内容解码为声明的元素类型的流,并返回一个带有状态、头信息和解码后的响应体流的 ResponseEntity

你也可以使用在 ReactiveAdapterRegistry 中注册的任何其他 async 或 reactive 类型。

1.3.3. 异常处理

默认情况下,WebClient 会对 4xx 和 5xx HTTP状态代码引发 WebClientResponseException。要定制这一点,你可以注册一个响应状态 handler,适用于通过客户端执行的所有响应:

WebClient webClient = WebClient.builder()
        .defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
        .build();

WebClientAdapter clientAdapter = WebClientAdapter.forClient(webClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory
        .builder(clientAdapter).build();

关于更多的细节和选项,例如抑制错误状态代码,请参见 WebClient.BuilderdefaultStatusHandler 的 Javadoc。

2. JMS (Java Message Service)

Spring 提供了一个 JMS 集成框架,它简化了JMS API 的使用,其方式与 Spring 对 JDBC API 的集成一样。

JMS可以大致分为两个方面的功能,即消息的生产和消费。JmsTemplate 类用于消息生产和同步消息接收。对于类似于Jakarta EE的消息驱动Bean风格的异步接收,Spring提供了一些消息监听器容器,你可以用它来创建消息驱动POJO(MDP)。Spring还提供了一种声明性的方式来创建消息监听器。

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

The org.springframework.jms.support package provides JMSException translation functionality. The translation converts the checked JMSException hierarchy to a mirrored hierarchy of unchecked exceptions. If any provider-specific subclasses of the checked jakarta.jms.JMSException exist, this exception is wrapped in the unchecked UncategorizedJmsException.

org.springframework.jms.support 包提供 JMSException 翻译功能。该翻译功能将受检查的 JMSException 层次结构转换为未受检查的异常的镜像层次结构。如果有任何受检查的 jakarta.jms.JMSException 的特定提供者子类存在,这个异常会被包裹在未受检查的 UncategorizedJmsException 中。

“未受检查的异常”,就是 RuntimeException 的子类,不需要调用者抛出或者`catch`。

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

org.springframework.jms.support.destination 包提供了管理 JMS 目的地(destination)的各种策略,比如为存储在JNDI中的 destination 提供一个服务定位器(service locator)。

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

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

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

从Spring Framework 5开始,Spring的JMS包完全支持JMS 2.0,并要求在运行时存在JMS 2.0 API。我们建议使用兼容JMS 2.0的提供者(provider)。

如果你碰巧在你的系统中使用了一个旧的消息代理(message broker),你可以尝试为你现有的 broker 代升级到JMS 2.0兼容的驱动程序。另外,你也可以尝试针对基于JMS 1.1的驱动运行,只需将JMS 2.0 API jar放在classpath上,但只使用JMS 1.1兼容的API来对付你的驱动。Spring的JMS支持在默认情况下遵守JMS 1.1的约定,所以通过相应的配置,它确实支持这样的场景。但是,请考虑这只适用于过渡场景。

2.1. 使用 Spring JMS

本节介绍了如何使用Spring的JMS组件。

2.1.1. 使用 JmsTemplate

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

使用 JmsTemplate 的代码只需要实现回调接口,给他们一个明确定义的高层契约。MessageCreator 回调接口在给定 JmsTemplate 中调用代码提供的 Session 时创建一个消息。为了允许更复杂地使用JMS API,SessionCallback 提供了JMS session,ProducerCallback 暴露了一个 SessionMessageProducer 对。

JMS API暴露了两种类型的发送方法,一种是将 delivery 模式、优先级和生存时间作为服务质量(QOS)参数,另一种是不接受QOS参数,使用默认值。由于 JmsTemplate 有许多发送方法,设置QOS参数被暴露为Bean属性以避免发送方法数量的重复。同样地,同步接收调用的超时值也是通过使用 setReceiveTimeout 属性来设置的。

一些JMS提供者允许通过 ConnectionFactory 的配置以 administratively 方式设置默认的QOS值。这样做的结果是,调用 MessageProducer 实例的 send 方法( send(Destination destination, Message message))使用的QOS默认值与JMS规范中规定的不同。为了提供一致的QOS值管理,JmsTemplate 必须通过将 boolean 属性 isExplicitQosEnabled 设置为 true 来专门启用自己的QOS值。

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

JmsTemplate 类的实例一旦配置好,就是线程安全的。这一点很重要,因为它意味着你可以配置一个 JmsTemplate 的单个实例,然后安全地将这个共享引用注入到多个协作者中。明确地说,JmsTemplate 是有状态的,因为它维护着对 ConnectionFactory 的引用,但这个状态不是对话式状态。

从 Spring Framework 4.1 开始,JmsMessagingTemplate 建立在 JmsTemplate 之上,并提供了与消息传递抽象的集成,即 org.springframework.messaging.Message。这让你能够以一种通用的方式创建要发送的消息。

2.1.2. 连接

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

当在EJB内使用JMS时,供应商提供了JMS接口的实现,这样它们就可以参与声明性事务管理并执行连接和会话的池化。为了使用这个实现,Jakarta EE容器通常要求你在EJB或servlet部署描述符中声明一个JMS连接工厂作为 resource-ref。为了确保在EJB内使用 JmsTemplate 的这些功能,客户程序应该确保它引用 ConnectionFactory 的管理实现。

缓存消息资源

标准的API涉及创建许多中间对象。为了发送一个消息,要进行以下的 'API' 步骤:

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

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

使用 SingleConnectionFactory

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

使用 CachingConnectionFactory

CachingConnectionFactory 扩展了 SingleConnectionFactory 的功能并增加了对 SessionMessageProducerMessageConsumer 实例的缓存。初始缓存大小被设置为 1,你可以使用 sessionCacheSize 属性来增加缓存会话(session)的数量。请注意,实际缓存的会话数量多于这个数字,因为会话是根据其确认模式(acknowledgment mode)来缓存的,所以当 sessionCacheSize 设置为 1 时,最多可以有四个缓存的会话实例(每种确认模式一个)。MessageProducerMessageConsumer 实例在它们自己的会话中被缓存,在缓存时也会考虑到生产者和消费者的独特属性。MessageProducer 根据其 destination 进行缓存。MessageConsumers 根据由 destination、selector、noLocal delivery 标志和持久订阅名称(如果创建持久消费者)组成的key进行缓存。

临时队列和主题(TemporaryQueue/TemporaryTopic)的 MessageProducersMessageConsumers 将永远不会被缓存。不幸的是,WebLogic JMS恰好在其常规的 destination 实现上实现了临时队列/主题(topic)接口,错误地提示其 destination 都不能被缓存。请在WebLogic上使用不同的连接池/缓存,或者为WebLogic目的定制 CachingConnectionFactory

2.1.3. Destination 管理

目的地(destination)作为 ConnectionFactory 实例,是JMS管理的对象,你可以在JNDI中存储和检索。在配置Spring应用上下文时,你可以使用JNDI JndiObjectFactoryBean 工厂类或 <jee:jndi-lookup> 来对你的对象对JMS destination 的引用进行依赖注入。然而,如果应用程序中有大量的 destination ,或者有JMS提供者特有的高级 destination 管理功能,那么这种策略往往是很麻烦的。这种高级 destination 管理的例子包括创建动态 destination 或支持 destination 的分层命名空间。JmsTemplate 将 destination 名称的解析委托给一个实现 DestinationResolver 接口的JMS destination 对象。DynamicDestinationResolverJmsTemplate 使用的默认实现,它能适应动态 destination 的解析。还提供了一个 JndiDestinationResolver,作为JNDI中包含的 destination 的服务定位器,并且可以选择返回到 DynamicDestinationResolver 中包含的行为。

很多时候,JMS应用中使用的 destination 只有在运行时才知道,因此,在应用部署时不能以管理方式创建。这通常是因为在相互作用的系统组件之间有共享的应用逻辑,这些组件在运行时根据一个众所周知的命名惯例创建 destination 。尽管动态 destination 的创建不是JMS规范的一部分,但大多数供应商已经提供了这种功能。动态 destination 是用用户定义的名称创建的,这与临时 destination 不同,而且通常不在JNDI中注册。用于创建动态 destination 的API因供应商而异,因为与 destination 相关的属性是供应商特有的。然而,供应商有时会做出一个简单的实现选择,即无视JMS规范中的警告,使用 TopicSession createTopic(String topicName) 方法或 QueueSession createQueue(String queueName) 方法来创建一个具有默认 destination 属性的新 destination。根据供应商的实现,DynamicDestinationResolver 也可以创建一个物理 destination,而不是只解析一个。

布尔属性 pubSubDomain 用于配置 JmsTemplate,了解正在使用什么 JMS domain。默认情况下,这个属性的值是 false,表示要使用点对点域 Queues。这个属性(由 JmsTemplate 使用)决定了通过 DestinationResolver 接口的实现进行动态 destination 解析的行为。

你也可以通过属性 defaultDestinationJmsTemplate 配置一个默认的 destination。默认 destination 是与发送和接收操作一起进行的,没有提到具体的 destination。

2.1.4. 消息监听器容器

在EJB世界中,JMS消息最常见的用途之一是驱动消息驱动的Bean(MDBs)。Spring提供了一种创建消息驱动的POJO(MDP)的解决方案,这种方式不会将用户与EJB容器绑在一起。(请参阅 异步接收: 消息驱动的POJO,详细介绍Spring的MDP支持)。从Spring Framework 4.1开始,端点方法可以用 @JmsListener 来注解—​更多细节请见 注解驱动的监听器端点

消息监听器容器用于从JMS消息队列中接收消息,并驱动被注入其中的 MessageListener。监听器容器负责所有消息接收的线程,并将其分配到监听器中进行处理。消息监听器容器是MDP和消息提供者之间的中介,负责注册接收消息、参与事务、资源获取和释放、异常转换等等。这让你可以编写与接收消息相关的(可能是复杂的)业务逻辑(也可能是对消息的响应),并将模板式的JMS基础设施问题委托给框架。

有两个标准的JMS消息监听器容器与Spring打包在一起,每一个都有其专门的功能集。

使用 SimpleMessageListenerContainer

这个消息监听器容器是两个标准类型中比较简单的。它在启动时创建了固定数量的JMS会话和消费者,通过使用标准的JMS MessageConsumer.setMessageListener() 方法来注册监听器,并让JMS提供者来执行监听器回拨。这个变体不允许动态适应运行时的需求,也不允许参与外部管理的事务。兼容性方面,它非常接近独立的JMS规范的精神,但通常与Jakarta EE的JMS限制不兼容。

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

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

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

这个容器也有可恢复的能力,当 broker 发生故障时。默认情况下,一个简单的 BackOff 实现每五秒重试一次。你可以指定一个自定义的 BackOff 实现以获得更精细的恢复选项。参见 ExponentialBackOff 以了解一个例子。

像它的兄弟姐妹(SimpleMessageListenerContainer)一样, DefaultMessageListenerContainer 支持本地JMS事务,并允许自定义确认模式(acknowledgment mode)。如果你的情况可行的话,强烈建议你使用这种方式而不是外部管理的事务—​也就是说,如果你能忍受JVM死亡时偶尔出现的重复消息的话。业务逻辑中的自定义重复消息检测步骤可以涵盖这种情况—​例如,以业务实体存在性检查或协议表检查的形式。任何这样的安排都比另一种方法要有效得多:用XA事务(通过用 JtaTransactionManager 配置 DefaultMessageListenerContainer)来包装你的整个处理过程,以涵盖JMS消息的接收以及消息监听器中业务逻辑的执行(包括数据库操作,等等)。
默认的 AUTO_ACKNOWLEDGE 模式并没有提供适当的可靠性保证。当监听器执行失败时(因为提供者在监听器调用后自动确认每条消息,没有异常要传播给提供者)或监听器容器关闭时(你可以通过设置 acceptMessagesWhileStopping 标志来配置),消息会丢失。在有可靠性需求的情况下,请确保使用事务会话(例如,可靠的队列处理和持久的主题订阅)。

2.1.5. 事务管理

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

在Jakarta EE环境中,ConnectionFactory 汇集了 ConnectionSession 实例,因此这些资源可以在不同的事务中有效地重复使用。在独立的环境中,使用Spring的 SingleConnectionFactory 会产生一个共享的JMS Connection,而每个事务都有自己独立的 Session。另外,也可以考虑使用供应商特定的池化适配器,如 ActiveMQ 的 PooledConnectionFactory 类。

你也可以将 JmsTemplateJtaTransactionManager 和具有XA功能的JMS ConnectionFactory 一起使用来执行分布式事务。注意,这需要使用JTA事务管理器以及正确配置的XA ConnectionFactory。(检查你的Jakarta EE服务器或JMS供应商的文档)。

在使用JMS API从 Connection 中创建 Session 时,在受管和非受管事务环境中重复使用代码可能会造成混乱。这是因为JMS API只有一个工厂方法来创建 Session,而且它需要事务和确认模式(acknowledgment mode)的值。在托管环境中,设置这些值是环境的事务性基础设施的责任,所以这些值被供应商对 JMS 连接的 wrapper 忽略了。当你在非管理环境中使用 JmsTemplate 时,你可以通过使用属性 sessionTransactedsessionAcknowledgeMode 指定这些值。当你用 JmsTemplate 使用 PlatformTransactionManager 时,模板总是被赋予一个事务性JMS Session

2.2. 发送消息

JmsTemplate 包含许多方便的方法来发送消息。发送方法通过使用 jakarta.jms.Destination 对象来指定目的地(destination),而其他方法则通过使用 JNDI lookup 中的 String 来指定目的地。没有接受目的地参数的 send 方法使用默认的目的地。

下面的例子使用 MessageCreator 回调,从提供的 Session 对象创建一个文本消息:

import jakarta.jms.ConnectionFactory;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.Queue;
import jakarta.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) 方法让你通过使用 destination 的字符串名称来发送一个消息。如果这些名字在JNDI中注册,你应该将模板的 destinationResolver 属性设置为 JndiDestinationResolver 的一个实例。

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

2.2.1. 使用消息转换器(Converter)

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

sandbox 目前包括一个 MapMessageConverter,它使用反射在 JavaBean 和 MapMessage 之间转换。其他流行的实现选择,你可能会自己实现,这些转换器使用现有的XML marshalling 包(如JAXB或 XStream)来创建一个代表对象的 TextMessage

为了适应对消息的属性(properties)、header 和 body 的设置,而这些设置又不能被通用地封装在一个转换器类中, MessagePostProcessor 接口让你在消息被转换后但在发送前访问它。下面的例子显示了如何在 java.util.Map 被转换为消息后修改消息 header 和属性:

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

2.2.2. 使用 SessionCallbackProducerCallback

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

2.3. 接收消息

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

2.3.1. 同步接收

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

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

Spring还通过使用 @JmsListener 注解来支持注解式监听器端点,并提供了一个开放的基础设施来以编程方式注册端点。到目前为止,这是设置异步接收器的最方便的方法。更多细节请参见 启用监听器端点注解

与EJB世界中的 Message-Driven Bean(MDB)类似,Message-Driven POJO(MDP)充当JMS消息的接收器。对MDP的一个限制(但见 使用 MessageListenerAdapter)是它必须实现 jakarta.jms.MessageListener 接口。注意,如果你的POJO在多个线程上接收消息,必须确保你的实现是线程安全的。

下面的例子显示了一个MDP的简单实现:

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

public class ExampleListener implements MessageListener {

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

一旦你实现了你的 MessageListener,现在是时候创建一个消息监听器容器了。

下面的例子展示了如何定义和配置Spring中的一个消息监听器容器(在本例中是 DefaultMessageListenerContainer):

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

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

请参阅各种消息监听器容器(所有这些容器都实现了 MessageListenerContainer)的 Spring javadoc,了解每种实现所支持的功能的完整描述。

2.3.3. 使用 SessionAwareMessageListener 接口

SessionAwareMessageListener 接口是一个Spring特定的接口,它提供了与JMS MessageListener 接口类似的契约,但也让消息处理方法访问接收 Message 的JMS Session。下面的列表显示了 SessionAwareMessageListener 接口的定义:

package org.springframework.jms.listener;

public interface SessionAwareMessageListener {

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

如果你希望你的 MDP 能够响应任何收到的消息(通过使用 onMessage(Message, Session) 方法中提供的 Session),你可以选择让你的 MDP 实现此接口(优先于标准 JMS MessageListener 接口)。Spring 中的所有消息监听器容器实现都支持实现 MessageListenerSessionAwareMessageListener 接口的 MDP。实现 SessionAwareMessageListener 的类需要注意的是,它们会通过接口与Spring绑定在一起。是否使用它,完全取决于你作为应用开发者或架构师的选择。

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

2.3.4. 使用 MessageListenerAdapter

MessageListenerAdapter 类是Spring异步消息传递支持的最后一个组件。简而言之,它可以让你将几乎所有的类作为一个MDP来公开(尽管有一些限制)。

考虑下面的接口定义:

public interface MessageDelegate {

    void handleMessage(String message);

    void handleMessage(Map message);

    void handleMessage(byte[] message);

    void handleMessage(Serializable message);
}

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

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

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

特别是注意到前面的 MessageDelegate 接口的实现(DefaultMessageDelegate 类)根本没有JMS的依赖。它确实是一个POJO,我们可以通过下面的配置把它变成一个MDP:

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

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

下一个例子显示了另一个MDP,它可以只处理接收JMS TextMessage 消息。请注意消息处理方法实际上被称为 receiveMessageListenerAdapter 中的消息处理方法的名称默认为 handleMessage),但它是可配置的(正如你在本节后面看到的)。还请注意 receive(..) 方法是如何被强类型化的,只接收和响应JMS TextMessage 消息。下面的列表显示了 TextMessageDelegate 接口的定义:

public interface TextMessageDelegate {

    void receive(TextMessage message);
}

下面的列表显示了一个实现 TextMessageDelegate 接口的类:

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

那么,attendant MessageListenerAdapter 的配置将如下:

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

请注意,如果 messageListener 收到的JMS Message 的类型不是 TextMessage,就会抛出一个 IllegalStateException(随后被吞噬)。MessageListenerAdapter 类的另一个功能是,如果一个处理方法返回一个非 void 的值,它能够自动送回一个响应的 Message。考虑一下下面的接口和类:

public interface ResponsiveTextMessageDelegate {

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

如果你将 DefaultResponsiveTextMessageDelegateMessageListenerAdapter 一起使用,任何从 'receive(..)' 方法执行中返回的非 null 值都会(在默认配置下)转换成一个 TextMessage。产生的 TextMessage 将被发送到原始 Message 的JMS Reply-To 属性中定义的 Destination(如果有的话)或 MessageListenerAdapter 上设置的默认 Destination(如果已经配置了一个)。如果没有找到 Destination,就会抛出一个 InvalidDestinationException(注意,这个异常不会被吞噬,而是在调用栈中传播)。

2.3.5. 在事务中处理消息

在一个事务中调用一个消息监听器只需要重新配置监听器容器。

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

考虑到以下的 bean 定义:

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

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

为了配置一个用于参与XA事务的消息监听器容器,你要配置一个 JtaTransactionManager(默认情况下,它委托给Jakarta EE服务器的事务子系统)。请注意,底层的JMS ConnectionFactory 需要具备XA能力,并正确地注册到你的JTA事务协调器上。(检查你的Jakarta EE服务器的JNDI资源配置。)这让消息接收和(例如)数据库访问成为同一个事务的一部分(具有统一的提交语义,代价是XA事务日志的开销)。

下面的Bean定义创建了一个事务管理器:

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

然后我们需要把它添加到我们早期的容器配置中。容器会处理剩下的事情。下面的例子显示了如何做到这一点:

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

2.4. 支持JCA消息端点

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

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

另外,你可以用一个给定的 ActivationSpec 对象来设置一个 JmsMessageEndpointManagerActivationSpec 对象也可以来自JNDI lookup(使用 <jee:jndi-lookup>)。下面的例子展示了如何做到这一点:

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

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

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

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

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

参见 JmsMessageEndpointManager、https://docs.spring.io/spring-framework/docs/6.0.8-SNAPSHOT/javadoc-api/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.html[JmsActivationSpecConfig],以及 ResourceAdapterFactoryBean 了解更多细节。

Spring还提供了一个不与JMS绑定的通用JCA消息端点管理器:org.springframework.jca.endpoint.GenericMessageEndpointManager。这个组件允许使用任何消息监听器类型(如JMS MessageListener)和任何提供者特定的 ActivationSpec 对象。请参阅你的JCA提供者的文档以了解你的连接器的实际能力,并参阅 GenericMessageEndpointManager javadoc 以了解Spring特定的配置细节。

基于JCA的消息端点管理非常类似于EJB 2.1的消息驱动 Bean。它使用相同的底层资源提供者 contract。与EJB 2.1 MDB一样,你也可以在Spring context 中使用JCA提供者支持的任何消息监听器接口。尽管如此,Spring还是为JMS提供了明确的 "便利" 支持,因为JMS是与JCA端点管理 contract 一起使用的最常见的端点API。

2.5. 注解驱动的监听器端点

异步接收消息的最简单方法是使用注解的监听器端点基础设施。简而言之,它可以让你把一个托管Bean的方法作为JMS监听器端点公开。下面的例子展示了如何使用它:

@Component
public class MyService {

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

前面例子的意思是,只要 jakarta.jms.Destination myDestination 上有消息, processOrder 方法就会被相应地调用(在这种情况下,用 JMS message 的内容,类似于 MessageListenerAdapter 所提供的)。

通过使用 JmsListenerContainerFactory,注解的端点基础设施在幕后为每个注解的方法创建一个消息监听器容器。这样的容器并没有针对 application contex t 进行注册,但可以通过使用 JmsListenerEndpointRegistry Bean轻松定位,以达到管理目的。

@JmsListener 在Java 8上是一个可重复的注解,所以你可以通过向它添加额外的 @JmsListener 声明,将几个 JMS destination 与同一个方法联系起来。

2.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 作为 factory 的来源,用来创建消息监听器容器。在这种情况下(并忽略JMS基础设施的设置),你可以调用 processOrder 方法,其 core poll size 为三个线程,maximum pool size 为十个线程。

你可以为每个注解定制要使用的监听器容器工厂,或者你可以通过实现 JmsListenerConfigurer 接口来配置一个明确的默认值。只有当至少有一个端点被注册而没有特定的容器工厂时,才需要默认。请参阅实现 JmsListenerConfigurer 的类的 javadoc,了解细节和示例。

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

<jms:annotation-driven/>

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

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

2.5.3. 注解式端点方法的签名

到目前为止,我们一直在我们的端点中注入一个简单的 String,但它实际上可以有一个非常灵活的方法签名。在下面的例子中,我们重写它来注入带有自定义 header 的 Order

@Component
public class MyService {

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

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

  • 原始的 jakarta.jms.Message 或其任何子类(只要它与传入的消息类型相匹配)。 *jakarta.jms.Session 用于对本地JMS API的可选访问(例如,用于发送自定义回复)。

  • org.springframework.messaging.Message,表示传入的JMS消息。请注意,这个消息同时拥有自定义和标准 header 信息(如 JmsHeaders 所定义)。

  • @Header 注解的法参数,提取特定的 header 值,包括标准的JMS header。

  • 一个 @Headers 注解的参数,也必须可分配给 java.util.Map,以获得对所有 header 的访问。

  • 不是支持的类型(MessageSession)之一的非注解元素被认为是 payload。你可以通过给参数加上 @Payload 的注解来明确这一点。你也可以通过添加一个额外的 @Valid 来开启验证。

注入 Spring 的 Message 抽象的能力特别有用,可以从存储在 transport 专用消息中的所有信息中获益,而不需要依赖 transport 专用API。下面的例子展示了如何做到这一点:

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

方法参数的处理由 DefaultMessageHandlerMethodFactory 提供,你可以进一步定制,以支持额外的方法参数。你也可以在那里定制转换和验证支持。

例如,如果我们想在处理 Order 之前确保它是有效的,我们可以用 @Valid 注解 payload,并配置必要的验证器(validator),如下例所示:

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

2.5.4. 响应管理

MessageListenerAdapter 的现有支持已经允许你的方法有一个非 void 的返回类型。在这种情况下,调用的结果被封装在一个 jakarta.jms.Message 中,被发送到原始消息的 JMSReplyTo header 中指定的 destination 或监听器上配置的默认 destination。现在你可以通过使用消息传递抽象的 @SendTo 注解来设置该默认 destination。

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

@JmsListener(destination = "myDestination")
@SendTo("status")
public OrderStatus processOrder(Order order) {
    // order processing
    return status;
}
如果你有几个 @JmsListener 注解的方法,你也可以把 @SendTo 注解放在类的层面上,以共享一个默认的回复 destination。

如果你需要以独立于 transport 的方式设置额外的 header,你可以返回一个 Message 来代替,方法类似于下面:

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

如果你需要在运行时计算响应 destination,你可以将你的响应封装在一个 JmsResponse 实例中,该实例也提供了运行时使用的 destination。我们可以把前面的例子改写成如下:

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

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

@Configuration
@EnableJms
public class AppConfig {

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

2.6. JMS命名空间支持

Spring 提供了一个 XML 命名空间来简化 JMS 配置。要使用 JMS 命名空间的元素,你需要引用 JMS schema,如下例所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jms="http://www.springframework.org/schema/jms" (1)
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jms
        https://www.springframework.org/schema/jms/spring-jms.xsd">

    <!-- bean definitions here -->

</beans>
1 引用 JMS schema。

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

<jms:listener-container>

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

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

</jms:listener-container>

前面的例子相当于创建两个不同的监听器容器bean定义和两个不同的 MessageListenerAdapter bean 定义,如 使用 MessageListenerAdapter 所示。除了前面的例子中显示的属性外,listener 元素还可以包含几个可选的属性。下表描述了所有的可用属性:

Table 3. JMS <listener> 元素的属性
属性 说明

id

托管监听器容器的Bean名称。如果没有指定,将自动生成一个Bean名称。

destination (required)

这个监听器的 destination 名称,通过 DestinationResolver 策略解析。

ref (required)

处理程序对象的Bean名称。

method

要调用的 handler method 的名称。如果 ref 属性指向一个 MessageListener 或Spring SessionAwareMessageListener,你可以省略这个属性。

response-destination

发送响应信息的默认响应 destination 的名称。这适用于不带有 JMSReplyTo 字段的请求消息的情况。这个 destination 的类型由监听器容器的 response-destination-type 属性决定。请注意,这只适用于有返回值的监听器方法,每个结果对象都被转换为响应消息。

subscription

持久订阅的名称,如果有的话。

selector

这个监听器的一个可选的消息选择器。

concurrency

为这个监听器启动的并发会话或消费者的数量。这个值可以是一个简单的数字,表示最大数量(例如,5),也可以是一个范围,表示下限和上限(例如,3-5)。注意,指定的最小值只是一个提示,在运行时可能会被忽略。默认值是由容器提供的值。

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

你可以自动地将这种设置作为 JmsListenerContainerFactory 来公开,通过 factory-id 属性指定要公开的 bean 的 id,如下例所示:

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

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

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

</jms:listener-container>

下表描述了所有可用的属性。请参阅 AbstractMessageListenerContainer 及其具体子类的类级 javadoc 以了解关于各个属性的更多细节。该 javadoc 还提供了关于事务选择和消息再交付(redelivery)场景的讨论。

Table 4. JMS <listener-container> 元素的属性
属性 说明

container-type

这个监听器容器的类型。可用的选项有 defaultsimpledefault102simple102(默认选项是 default)。

container-class

一个自定义的监听器容器实现类,作为一个全路径的类名。根据 container-type 属性,默认的是Spring的标准 DefaultMessageListenerContainerSimpleMessageListenerContainer

factory-id

将此元素定义的设置作为 JmsListenerContainerFactory 的指定 id 公开,这样它们就可以在其他端点上重复使用。

connection-factory

对JMS ConnectionFactory Bean的引用(默认bean名称为 connectionFactory)。

task-executor

对JMS监听器调用者的Spring TaskExecutor 的引用。

destination-resolver

对用于解析JMS Destination 实例的 DestinationResolver 策略的引用。

message-converter

对用于将JMS消息转换为监听器方法参数的 MessageConverter 策略的引用。默认是 SimpleMessageConverter

error-handler

ErrorHandler 策略的引用,用于处理在执行 MessageListener 过程中可能发生的任何未捕获的异常。

destination-type

这个监听器的JMS destination type:queuetopicdurableTopicsharedTopicsharedDurableTopic。这可能会启用容器的 pubSubDomainsubscriptionDurablesubscriptionShared 属性。默认是 queue(禁用这三个属性)。

response-destination-type

响应的JMS destination type:queuetopic。默认是 destination-type 属性的值。

client-id

这个监听器容器的JMS客户端ID。当你使用持久订阅时,你必须指定它。

cache

JMS资源的缓存级别:noneconnectionsessionconsumer 者,或 auto。默认情况下(auto),缓存级别是有效的消费者,除非指定了一个外部事务管理器—​在这种情况下,有效的默认值是 none(假设Jakarta EE风格的事务管理,其中给定的 ConnectionFactory 是一个XA感知池)。

acknowledge

本地JMS确认模式(acknowledge mode):autoclientdups-oktransactedTransacted 的值会激活一个本地事务的 Session。作为一种选择,你可以指定 transaction-manager 属性,在后面的表格中描述。默认是 auto

transaction-manager

对外部 PlatformTransactionManager(通常是基于XA的事务协调器,如Spring的 JtaTransactionManager)的引用。如果没有指定,就会使用本地确认(见 acknowledge 属性)。

concurrency

为每个监听器启动的并发会话或消费者的数量。它可以是一个简单的数字,表示最大数量(例如,5),也可以是一个范围,表示下限和上限(例如,3-5)。注意,指定的最小值只是一个提示,在运行时可能会被忽略。如果是主题监听器或者队列排序很重要,你应该把并发数限制在 1。对于一般的队列,可以考虑提高它。

prefetch

加载到一个会话中的最大信息数量。注意,提高这个数字可能导致并发消费者的饥饿。

receive-timeout

调用 receive 时使用的超时(以毫秒为单位)。默认是 1000(一秒钟)。-1 表示没有超时。

back-off

指定用于计算恢复尝试的间隔的 BackOff 实例。如果 BackOffExecution 实现返回 BackOffExecution#STOP,监听器容器就不会进一步尝试恢复。设置此属性时,recovery-interval 值被忽略。默认的是一个固定的 BackOff,间隔时间为 5000 毫秒(也就是5秒)。

recovery-interval

指定恢复尝试的间隔时间,以毫秒为单位。它提供了一个方便的方法来创建一个具有指定间隔的 FixedBackOff。对于更多的恢复选项,可以考虑指定一个 BackOff 实例来代替。默认是 5000 毫秒(即5秒)。

phase

这个容器应该在哪个生命周期阶段启动和停止。这个值越小,这个容器就越早开始,越晚停止。默认值是 Integer.MAX_VALUE,意味着容器尽可能晚地开始,尽可能快地停止。

如下面的例子所示,配置基于JCA的、支持 jms schema 的监听器容器是非常相似的:

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

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

</jms:jca-listener-container>

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

Table 5. JMS <jca-listener-container/> 元素的属性
属性 说明

factory-id

将此元素定义的设置作为 JmsListenerContainerFactory 的指定 id 公开,这样它们就可以在其他端点上重复使用。

resource-adapter

对JCA ResourceAdapter Bean 的引用(默认的bean名称是 resourceAdapter)。

activation-spec-factory

JmsActivationSpecFactory 的引用。默认是自动检测 JMS 提供者及其 ActivationSpec 类(参见 DefaultJmsActivationSpecFactory)。

destination-resolver

对用于解析JMS DestinationsDestinationResolver 策略的引用。

message-converter

对用于将 JMS 消息转换为监听器方法参数的 MessageConverter 策略的引用。默认是 SimpleMessageConverter

destination-type

这个监听器的JMS destination type:queuetopicdurableTopicsharedTopicsharedDurableTopic。这可能会启用容器的 pubSubDomainsubscriptionDurablesubscriptionShared 属性。默认是 queue(禁用这三个属性)。

response-destination-type

响应的JMS destination type:queuetopic。默认是 destination-type 属性的值。

client-id

这个监听器容器的JMS客户端ID。当使用持久订阅时,需要指定它。

acknowledge

原生 JMS 确认模式:autoclientdups-oktransactedTransacted 的值会激活一个本地事务的 Session。作为一种选择,你可以指定后面描述的 transaction-manager 属性。默认是 auto

transaction-manager

对 Spring JtaTransactionManagerjakarta.transaction.TransactionManager 的引用,用于为每个传入的消息启动一个XA事务。如果没有指定,就会使用本地确认(见 acknowledge 属性)。

concurrency

为每个监听器启动的并发会话或消费者的数量。它可以是一个表示最大数量的简单数字(例如`5`),也可以是一个表示下限和上限的范围(例如 3-5)。注意,指定的最小值只是一个提示,当你使用JCA监听器容器时,通常在运行时被忽略。默认值是 1

prefetch

加载到一个会话中的最大信息数量。注意,提高这个数字可能导致并发消费者的饥饿。

3. JMX

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

JMX?

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

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

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

  • 一个灵活的机制,用于控制你的bean的管理接口

  • 通过远程、JSR-160 连接器(connector) 对MBeans进行声明性暴露。

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

这些功能被设计为无需将你的应用组件与Spring或JMX接口和类耦合。事实上,在大多数情况下,你的应用程序类不需要知道Spring或JMX,就可以利用Spring JMX的功能。

3.1. 将你的Bean导出到JMX

Spring的JMX框架的核心类是 MBeanExporter。这个类负责把你的Spring Bean带到JMX MBeanServer 上注册。例如,请看下面这个类:

package org.springframework.jmx;

public class JmxTestBean implements IJmxTestBean {

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

    public int getAge() {
        return age;
    }

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

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

    public String getName() {
        return name;
    }

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

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

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

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

前面的配置片段中相关的Bean定义是 exporter Bean。beans 属性告诉 MBeanExporter 你的哪些Bean必须被导出到JMX MBeanServer。在默认配置中, beans Map 中每个条目的key被用作相应条目value所引用的bean的 ObjectName。你可以改变这种行为,如 为你的 Bean 控制 ObjectName 实例 中所述。

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

MBeanExporter 是一个 Lifecycle Bean(见 Startup 和 Shutdown 回调)。默认情况下,MBeans在应用程序的生命周期中尽可能晚地被导出。你可以配置导出发生的 phase,或者通过设置 autoStartup 标志来禁用自动注册。

3.1.1. 创建 MBeanServer

上一节 所示的配置假定应用程序运行在已经有一个(而且只有一个) MBeanServer 的环境中。在这种情况下,Spring 会尝试定位正在运行的 MBeanServer,并在该服务器上注册你的bean(如果有的话)。当你的应用程序在拥有自己的 MBeanServer 的容器(如Tomcat或IBM WebSphere)内运行时,这种行为很有用。

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

<beans>

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

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

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

</beans>

在前面的例子中,MBeanServer 的一个实例由 MBeanServerFactoryBean 创建,并通过 server 属性提供给 MBeanExporter。当你提供你自己的 MBeanServer 实例时,MBeanExporter 不会尝试定位一个正在运行的 MBeanServer,而是使用提供的 MBeanServer 实例。为了使其正常工作,你必须在你的 classpath 上有一个JMX实现。

3.1.2. 重用已有的 MBeanServer

如果没有指定服务器,MBeanExporter 会尝试自动检测一个正在运行的 MBeanServer。这在大多数环境中是可行的,因为在这些环境中只使用一个 MBeanServer 实例。然而,当存在多个实例时,导出器(exporter)可能会选择错误的服务器。在这种情况下,你应该使用 MBeanServer agentId 来指示要使用的实例,如下面的例子所示:

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

对于平台或现有的 MBeanServer 有一个动态的(或未知的)agentId,通过 lookup method 检索的情况,你应该使用 factory-method,如下例所示:

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

    <!-- other beans here -->

</beans>

3.1.3. 延迟初始化 MBean

如果你用 MBeanExporter 配置一个Bean,而该 MBeanExporter 也被配置为延迟初始化,那么 MBeanExporter 就不会破坏这个契约,并避免实例化Bean。相反,它向 MBeanServer 注册了一个代理,并推迟从容器中获取Bean,直到对代理的第一次调用发生。

3.1.4. MBean 的自动注册

任何通过 MBeanExporter 导出的bean,如果已经是有效的 MBeans,就会在 MBeanServer 上按原样注册,而无需Spring的进一步干预。你可以通过将 autodetect 属性设置为 true,使 MBeanExporter 自动检测到 MBean,如下例所示:

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

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

在前面的例子中,名为 spring:mbean=true 的Bean已经是一个有效的JMX MBean,并被Spring自动注册。默认情况下,一个被自动检测到的JMX注册的Bean,其Bean名被用作 ObjectName。你可以覆盖这一行为,详见 为你的 Bean 控制 ObjectName 实例

3.1.5. 控制注册行为

考虑这样的情景:Spring MBeanExporter 试图通过使用 ObjectName bean:name=testBean1MBeanServer 注册一个 MBean。如果一个 MBean 实例已经在同一个 ObjectName 下注册了,那么默认行为就是失败(并抛出一个 InstanceAlreadyExistsException)。

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

Table 6. 注册行为
注册行为 说明

FAIL_ON_EXISTING

这是默认的注册行为。如果一个 MBean 实例已经在相同的 ObjectName 下被注册,那么正在被注册的 MBean 不会被注册,并且会抛出一个 InstanceAlreadyExistsException。现有的 MBean 不受影响。

IGNORE_EXISTING

如果一个 MBean 实例已经在相同的 ObjectName 下被注册,那么正在被注册的 MBean 不会被注册。现有的 MBean 不受影响,也不会抛出 Exception。这在多个应用程序想在共享的 MBeanServer 中共享一个共同的 MBean 的情况下是很有用的。

REPLACE_EXISTING

如果一个 MBean 实例已经在相同的 ObjectName 下注册了,那么以前注册的现有 MBean 就会被取消注册,而新的 MBean 就会在其位置上被注册(新的 MBean 有效地取代了以前的实例)。

前述表格中的值被定义为 RegistrationPolicy 类的枚举。如果你想改变默认的注册行为,你需要将 MBeanExporter 定义中的 registrationPolicy 属性的值设置为这些值之一。

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

<beans>

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

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

</beans>

3.2. 控制你的 Bean 的管理接口

上一节 的例子中,你对Bean的管理接口(management interface)几乎没有控制。每个导出的Bean的所有 public 属性和方法都分别作为JMX属性和操作被暴露。为了对导出Bean的哪些属性和方法实际暴露为JMX属性和操作进行更精细的控制,Spring JMX提供了一个全面的、可扩展的机制来控制Bean的管理接口。

3.2.1. 使用 MBeanInfoAssembler 接口

在幕后,MBeanExporter 委托给 org.springframework.jmx.export.assembler.MBeanInfoAssembler 接口的实现,该接口负责定义每个被暴露的bean的管理接口。默认的实现,org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler,定义了一个管理接口,暴露了所有的 public 属性和方法(正如你在前面几节的例子中看到的)。Spring 提供了 MBeanInfoAssembler 接口的两个额外实现,让你通过使用源码级元数据或任何任意接口来控制生成的管理接口。

3.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 的注解,分别创建一个只写或只读的属性。

一个 ManagedResource 注解的Bean必须是 public 的,暴露操作或属性的方法也是如此。

下面的例子显示了我们在 创建 MBeanServer 中使用的 JmxTestBean 类的注解版本:

package org.springframework.jmx;

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

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

    private String name;
    private int age;

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

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

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

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

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

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

}

在前面的例子中,你可以看到 JmxTestBean 类被标记为 ManagedResource 注解,这个 ManagedResource 注解被配置为一组属性。这些属性可以用来配置由 MBeanExporter 生成的MBean的各个方面,在后面的 源级元数据类型 中会有更详细的解释。

agename 属性都被注解了 ManagedAttribute 注解,但是在 age 属性中,只有 getter 被标记。这导致这两个属性作为属性被包含在管理接口中,但是 age 属性是只读的。

最后,add(int, int) 方法被标记为 ManagedOperation 属性,而 dontExposeMe() 方法则没有。这导致在你使用 MetadataMBeanInfoAssembler 时,管理接口只包含一个操作(add(int, int))。

下面的配置显示了你如何配置 MBeanExporter 来使用 MetadataMBeanInfoAssembler

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

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

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

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

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

在前面的例子中,MetadataMBeanInfoAssembler Bean 已经被配置为 AnnotationJmxAttributeSource 类的一个实例,并通过 assembler 属性传递给 MBeanExporter。这就是为你的Spring暴露的 MBean 利用元数据驱动的管理接口所需要的一切。

3.2.3. 源级元数据类型

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

Table 7. 源级元数据类型
目的 注解 注解类型

将一个 Class 的所有实例标记为JMX管理的资源。

@ManagedResource

Class

将一个方法标记为JMX操作。

@ManagedOperation

Method

将一个getter或setter标记为JMX属性的一半。

@ManagedAttribute

Method (only getters and setters)

定义操作参数的描述。

@ManagedOperationParameter and @ManagedOperationParameters

Method

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

Table 8. 源级元数据参数
参数 说明 适用于

ObjectName

MetadataNamingStrategy 使用,以确定被管理资源的 ObjectName

description

设置资源、属性或操作的友好描述。

ManagedResource, ManagedAttribute, ManagedOperationManagedOperationParameter

currencyTimeLimit

设置 currencyTimeLimit 描述符字段的值。

ManagedResourceManagedAttribute

defaultValue

设置 defaultValue 描述符字段的值。

ManagedAttribute

log

设置 log 描述符字段的值。

ManagedResource

logFile

设置 logFile 描述符字段的值。

ManagedResource

persistPolicy

设置 persistPolicy 描述符字段的值。

ManagedResource

persistPeriod

设置 persistPeriod 描述符字段的值。

ManagedResource

persistLocation

设置 persistLocation 描述符字段的值。

ManagedResource

persistName

设置 persistName 描述符字段的值。

ManagedResource

name

设置一个操作参数的显示名称。

ManagedOperationParameter

index

3.2.4. 使用 AutodetectCapableMBeanInfoAssembler 注解

为了进一步简化配置,Spring包括 AutodetectCapableMBeanInfoAssembler 接口,它继承了 MBeanInfoAssembler 接口,以增加对 MBean 资源的自动检测支持。如果你用 AutodetectCapableMBeanInfoAssembler 的实例来配置 MBeanExporter,它就可以 “vote” (投票)决定是否将Bean暴露给JMX。

AutodetectCapableMBeanInfo 接口的唯一实现是 MetadataMBeanInfoAssembler,它投票包括任何标有 ManagedResource 属性的 bean。在这种情况下,默认的方法是使用 Bean 的名字作为 ObjectName,这将导致类似于下面的配置:

<beans>

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

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

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

</beans>

注意,在前面的配置中,没有Bean被传递给 MBeanExporter。然而,JmxTestBean 仍然被注册了,因为它被标记为 ManagedResource 属性,MetadataMBeanInfoAssembler 检测到了这一点,并投票(vote)将其包含在内。这种方法的唯一问题是,JmxTestBean 的名字现在有了业务意义。你可以通过改变 ObjectName 创建的默认行为来解决这个问题,该行为在 为你的 Bean 控制 ObjectName 实例 实例中定义。

3.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以使用该接口作为管理接口(management interface)的定义:

<beans>

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

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

</beans>

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

在前面的案例中,IJmxTestBean 接口被用来构建所有Bean的所有管理接口。在许多情况下,这并不是所期望的行为,你可能想为不同的Bean使用不同的接口。在这种情况下,你可以通过 interfaceMappings 属性向 InterfaceBasedMBeanInfoAssembler 传递一个 Properties 实例,其中每个条目的key是Bean名称,每个条目的value是为该Bean使用的接口名称的逗号分隔的列表。

如果没有通过 managedInterfacesinterfaceMappings 属性指定管理接口, InterfaceBasedMBeanInfoAssembler 就会反射 bean,并使用该 bean 实现的所有接口来创建管理接口。

3.2.6. 使用 MethodNameBasedMBeanInfoAssembler

MethodNameBasedMBeanInfoAssembler 让你指定一个方法名称的列表,这些方法名称作为属性和操作暴露给JMX。下面的代码显示了一个配置示例:

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

在前面的例子中,你可以看到 addmyOperation 方法被暴露为JMX操作,而 getName()setName(String)getAge() 被暴露为JMX属性的相应部分。在前面的代码中,方法映射适用于暴露于JMX的Bean。要在逐个bean的基础上控制方法的暴露,你可以使用 MethodNameMBeanInfoAssemblermethodMappings 属性来将bean名称映射到方法名称的列表。

3.3. 为你的 Bean 控制 ObjectName 实例

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

3.3.1. 从 Properties 中读取 ObjectName 实例

你可以配置你自己的 KeyNamingStrategy 实例,并将其配置为从 Properties 实例中读取 ObjectName 实例,而不是使用 Bean key。KeyNamingStrategy 试图在 Properties 中找到一个 key 与 bean key 对应的条目。如果没有找到条目或者 Properties 实例为 null,则使用 bean key 本身。

下面的代码显示了 KeyNamingStrategy 的一个配置样本:

<beans>

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

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

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

</beans>

前面的例子用 Properties 实例配置了 KeyNamingStrategy 的一个实例,该实例是由 mapping 属性定义的 Properties 实例和位于 mappings 属性定义的路径中的 properties 文件合并而成。在这个配置中,testBean Bean 被赋予 bean:name=testBean1ObjectName,因为这是 Properties 实例中的条目,它的 key 与 bean 的 key 对应。

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

3.3.2. 使用 MetadataNamingStrategy

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

<beans>

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

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

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

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

</beans>

如果没有为 ManagedResource 属性提供 objectName,那么将以下列格式创建一个 ObjectName: [全路径包名]:type=[short-classname],name=[bean-name]。例如,为下面的 bean 生成的 ObjectName 将是 com.example:type=MyClass,name=myBean

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

3.3.3. 配置基于注解的 MBean 导出

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

@Configuration
@EnableMBeanExport
public class AppConfig {

}

如果你喜欢基于XML的配置,<context:mbean-export/> 元素也有同样的作用,如下表所示:

<context:mbean-export/>

如果有必要,你可以提供一个对特定 MBean server 的引用,defaultDomain 属性(AnnotationMBeanExporter 的一个属性)接受生成的 MBean ObjectName domain 的替代值。如下面的例子所示,这被用来代替前面关于 MetadataNamingStrategy 一节中描述的全路径的包名称:

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

}

下面的例子显示了前面基于注解的例子的XML相应内容:

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

3.4. 使用 JSR-160 连接器(Connector)

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

3.4.1. 服务器端连接器

为了让Spring JMX创建、启动并公开 JSR-160 JMXConnectorServer,你可以使用以下配置:

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

默认情况下,ConnectorServerFactoryBean 创建了一个 JMXConnectorServer,绑定到 service:jmx:jmxmp://localhost:9875。因此,serverConnector bean通过 JMXMP 协议将本地 MBeanServer 暴露给客户端,端口为 9875 的本地主机。请注意,JMXMP协议被JSR 160规范标记为可选协议。目前,主要的开源JMX实现MX4J和JDK提供的JMXMP并不支持。

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

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

如果 ObjectName 属性被设置了,Spring 就会自动在该 ObjectName 下的 MBeanServer 中注册你的连接器。下面的例子显示了你在创建 JMXConnector 时可以传递给 ConnectorServerFactoryBean 的全部参数:

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

注意,当你使用基于RMI的连接器时,你需要启动 lookup service(tnameservrmiregistry),以便完成名称注册。

3.4.2. 客户端连接器

要创建一个 MBeanServerConnection 到一个支持JSR-160的远程 MBeanServer,你可以使用 MBeanServerConnectionFactoryBean,如下例所示:

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

3.4.3. 通过Hessian或SOAP的JMX

JSR-160允许对客户端和服务器之间的通信方式进行扩展。前面几节所示的例子使用了JSR-160规范要求的基于RMI的强制性实现(IIOP和JRMP)和(可选)JMXMP。通过使用其他提供者或JMX实现(如 MX4J),你可以利用SOAP或Hessian等协议,通过简单的HTTP或SSL和其他协议,如下面的例子所示:

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

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

3.5. 通过代理访问 MBean

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

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

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

MBeanProxyFactoryBean 可以为任何可以通过 MBeanServerConnection 访问的 MBean 创建一个代理。默认情况下,本地的 MBeanServer 被定位和使用,但是你可以覆盖这一点,提供一个指向远程 MBeanServerMBeanServerConnection,以满足指向远程 MBean 的代理:

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

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

在前面的例子中,我们创建了一个 MBeanServerConnection,指向一个使用 MBeanServerConnectionFactoryBean 的远程机器。这个 MBeanServerConnection 然后通过 server 属性传递给 MBeanProxyFactoryBean。被创建的代理通过这个 MBeanServerConnection 转发所有对 MBeanServer 的调用。

3.6. 通知

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

3.6.1. 为通知注册监听器

Spring的JMX支持使其能够轻松地将任意数量的 NotificationListeners 与任意数量的 MBean(这包括由Spring的 MBeanExporter 导出的 MBean 和通过其他机制注册的 MBean)注册在一起。例如,考虑这样的情景:当目标MBean的一个属性发生变化时,人们希望(通过 Notification)被告知。下面的例子将通知写到控制台:

package com.example;

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

public class ConsoleLoggingNotificationListener
        implements NotificationListener, NotificationFilter {

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

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

}

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

<beans>

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

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

</beans>

有了前面的配置,每次从目标 MBean(bean:name=testBean1)广播 JMX Notification 时,通过 notificationListenerMappings 属性注册为监听器的 ConsoleLoggingNotificationListener bean 就会被通知。然后, ConsoleLoggingNotificationListener Bean 可以采取它认为合适的任何行动来响应该 Notification

你也可以使用直接的 Bean 名称作为导出的Bean和监听器之间的联系,如下例所示:

<beans>

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

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

</beans>

如果你想为围绕 MBeanExporter 的所有Bean注册一个 NotificationListener 实例,你可以使用特殊的通配符(*)作为 notificationListenerMappings property map 中的一个条目的key,如下图所示:

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

如果你需要做相反的事情(也就是说,针对一个MBean注册一些不同的监听器),你必须使用 notificationListeners 列表属性(优先于 notificationListenerMappings 属性)。这一次,我们不是为单个MBean配置 NotificationListener,而是配置 NotificationListenerBean 实例。NotificationListenerBean 封装了一个 NotificationListenerObjectName(或 ObjectNames),它将在 MBeanServer 中被注册。NotificationListenerBean 还封装了一些其他的属性,比如 NotificationFilter 和一个任意的 handback 对象,可以在高级的JMX通知场景中使用。

使用 NotificationListenerBean 实例时的配置与之前介绍的没有太大的区别,如下例所示:

<beans>

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

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

</beans>

前面的例子等同于第一个通知的例子。那么,假设我们想在每次 Notification 被引发时得到一个 handback 对象,并且我们还想通过提供 NotificationFilter 来过滤掉不相干的 Notifications。下面的例子完成了这些目标:

<beans>

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

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

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

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

</beans>

(关于什么是 handback 对象,以及什么是 otificationFilter 的全面讨论,请参见JMX规范(1.2)中题为 "JMX通知模型" 的部分)。

3.6.2. 发布通知

Spring 不仅提供了对注册接收通知的支持,还提供了对发布通知的支持。

本节实际上只与 Spring 管理的 Bean 有关,这些 Bean 已经通过 MBeanExporter 作为 MBean 公开。任何现有的用户定义的 MBean 应该使用标准的JMX API来发布通知。

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

正如 NotificationPublisher 接口的 javadoc 中所述,通过 NotificationPublisher 机制发布事件的托管 Bean 不负责通知监听器的状态管理。Spring的JMX支持负责处理所有的JMX基础设施问题。作为一个应用开发者,你需要做的就是实现 NotificationPublisherAware 接口,并通过使用提供的 NotificationPublisher 实例开始发布事件。请注意,NotificationPublisher 是在被管理的 Bean 被注册到 MBeanServer 后设置的。

使用 NotificationPublisher 实例是非常直接的。你创建一个JMX Notification 实例(或一个适当的 Notification 子类的实例),用与要发布的事件相关的数据填充 notification ,并在 NotificationPublisher 实例上调用 sendNotification(Notification),传入该 Notification

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

package org.springframework.jmx;

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

public class JmxTestBean implements IJmxTestBean, NotificationPublisherAware {

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

    // other getters and setters omitted for clarity

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

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

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

}

NotificationPublisher 接口和让它工作的机制是Spring的JMX支持中比较好的功能之一。然而,它确实伴随着将你的类与Spring和JMX耦合的代价。一如既往,这里的建议是要务实。如果你需要 NotificationPublisher 提供的功能,并且你可以接受与Spring和JMX的耦合,那么就这样做吧。

3.7. 更多资源

本节包含关于JMX的进一步资源的链接:

4. 电子邮件

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

依赖库

为了使用Spring框架的电子邮件支持,你的应用程序的classpath上需要有以下JAR:

这个库在网上是免费提供的—​比如在 Maven Central,它的名字是 com.sun.mail:jakarta.mail。请确保使用最新的2.x版本(使用 jakarta.mail 包命名空间)而不是 Jakarta Mail 1.6.x(使用 javax.mail 包命名空间)。

Spring框架为发送电子邮件提供了一个有用的工具库,它使你免受底层邮件系统的影响,并负责代表客户端进行低级别的资源处理。

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

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

4.1. 用法

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

public interface OrderManager {

    void placeOrder(Order order);

}

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

4.1.1. 基本的 MailSenderSimpleMailMessage 用法

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

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

public class SimpleOrderManager implements OrderManager {

    private MailSender mailSender;
    private SimpleMailMessage templateMessage;

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

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

    public void placeOrder(Order order) {

        // Do the business calculations...

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

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

}

下面的例子显示了前述代码的Bean定义:

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

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

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

4.1.2. 使用 JavaMailSenderMimeMessagePreparator

本节介绍了 OrderManager 的另一个实现,它使用了 MimeMessagePreparator 回调接口。在下面的例子中,mailSender 属性的类型是 JavaMailSender,这样我们就能使用 JavaMail 的 MimeMessage 类:

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

import jakarta.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("mail@mycompany.example"));
                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());
        }
    }

}
邮件代码是一个跨领域的问题,很可能被重构为一个 自定义的 Spring AOP 切面,然后可以在 OrderManager 目标的适当 joinpoint 上运行。

Spring 框架的邮件支持是通过标准的 JavaMail 实现的。更多信息请参见相关的javadoc。

4.2. 使用 JavaMail MimeMessageHelper

org.springframework.mail.javamail.MimeMessageHelper 是一个在处理 JavaMail 消息时非常方便的类,它使你不必使用冗长的 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("test@host.com");
helper.setText("Thank you for ordering!");

sender.send(message);

4.2.1. 发送附件和内联资源

Multipart 电子邮件允许使用附件和内联资源。内联资源的例子包括你想在邮件中使用的图片或样式表,但你不希望以附件形式显示。

附件

下面的例子告诉你如何使用 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("test@host.com");

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("test@host.com");

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

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

sender.send(message);
内联资源是通过使用指定的 Content-ID(上例中的标识符 1234)添加到 MimeMessage 中。添加文本和资源的顺序是非常重要的。一定要先添加文本,然后再添加资源。如果你反其道而行之,就不会成功。

4.2.2. 通过使用模板库创建电子邮件内容

在前面的例子中,通过使用 message.setText(..) 这样的方法调用,代码明确地创建了电子邮件的内容。这对于简单的情况来说是没有问题的,在前面提到的例子中也是可以的,其目的是向你展示API的最基本内容。

不过,在你的典型企业应用中,由于一些原因,开发人员通常不会通过使用前面所示的方法来创建电子邮件的内容:

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

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

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

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

5. 任务执行和调度

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

Spring还提供了一些集成类,以支持与 Timer(自1.3以来是JDK的一部分)和 Quartz Scheduler 一起进行调度。你可以通过使用一个 FactoryBean 来设置这两个调度器,并分别对 TimerTrigger 实例进行可选的引用。此外,Quartz Scheduler 和 Timer 都有一个便利类,可以让你调用现有目标对象的方法(类似于正常的 MethodInvokingFactoryBean 操作)。

5.1. Spring 的 TaskExecutor 抽象

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

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

创建 TaskExecutor 的初衷是为其他Spring组件提供一个线程池的抽象。诸如 ApplicationEventMulticaster、JMS 的 AbstractMessageListenerContainer 和Quartz集成等组件都使用 TaskExecutor 抽象来进行线程池。然而,如果你的Bean需要线程池行为,你也可以使用这个抽象来满足自己的需求。

5.1.1. TaskExecutor 类型

Spring包含了许多预建的 TaskExecutor 实现。在所有的可能性中,你应该永远不需要实现你自己的。Spring提供的变体有以下几种:

  • SyncTaskExecutor: 这个实现没有异步运行调用。相反,每个调用都是在调用线程中进行的。它主要用于不需要多线程的情况,如简单的测试案例。

  • SimpleAsyncTaskExecutor: 这个实现没有重复使用任何线程。相反,它为每个调用启动了一个新的线程。然而,它确实支持一个并发限制,即阻止任何超过限制的调用,直到释放出一个 slot。如果你正在寻找真正的线程池,请参阅本列表后面的 ThreadPoolTaskExecutor

  • ConcurrentTaskExecutor: 这个实现是 java.util.concurrent.Executor 实例的一个适配器。有一个替代方案(ThreadPoolTaskExecutor),它将 Executor 的配置参数作为bean属性公开。很少有必要直接使用 ConcurrentTaskExecutor。然而,如果 ThreadPoolTaskExecutor 对你的需求来说不够灵活,ConcurrentTaskExecutor 是一个替代方案。

  • ThreadPoolTaskExecutor: 这个实现是最常用的。它暴露了用于配置 java.util.concurrent.ThreadPoolExecutor 的bean属性,并将其封装在一个 TaskExecutor 中。如果你需要适应不同种类的 java.util.concurrent.Executor,我们建议你使用 ConcurrentTaskExecutor 来代替。

  • DefaultManagedTaskExecutor: 这个实现在 JSR-236 兼容的运行环境(如 Jakarta EE 应用服务器)中使用一个JNDI获得的 ManagedExecutorService,为此取代了 CommonJ WorkManager。

5.1.2. 使用 TaskExecutor

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

import org.springframework.core.task.TaskExecutor;

public class TaskExecutorExample {

    private class MessagePrinterTask implements Runnable {

        private String message;

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

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

    private TaskExecutor taskExecutor;

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

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

正如你所看到的,你不是从池中检索一个线程并自己执行它,而是将你的 Runnable 添加到队列中。然后,TaskExecutor 使用其内部规则来决定何时运行该任务。

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

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

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

5.2. Spring 的 TaskScheduler 抽象

除了 TaskExecutor 抽象,Spring 还有一个 TaskScheduler SPI,它有多种方法用于调度任务,以便在未来的某个时间点运行。下面的列表显示了 TaskScheduler 接口的定义:

public interface TaskScheduler {

    Clock getClock();

    ScheduledFuture schedule(Runnable task, Trigger trigger);

    ScheduledFuture schedule(Runnable task, Instant startTime);

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

    ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);

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

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);

最简单的方法是名为 schedule 的方法,它只接受一个 Runnable 和一个 Instant。这将导致任务在指定时间后运行一次。所有其他的方法都能够调度任务重复运行。固定速率(fixed-rate)和固定延迟(fixed-delay)的方法用于简单的定期执行,但接受 Trigger 的方法要灵活得多。

5.2.1. Trigger 接口

Trigger 接口本质上是受JSR-236的启发。Trigger 的基本思想是,执行时间可以根据过去的执行结果甚至是任意条件来确定。如果这些确定考虑到了前面的执行结果,那么这些信息在 TriggerContext 中就可以得到。Trigger 接口本身是非常简单的,正如下面的列表所示:

public interface Trigger {

    Instant nextExecution(TriggerContext triggerContext);
}

TriggerContext 是最重要的部分。它封装了所有的相关数据,并且在将来必要时可以进行扩展。TriggerContext 是一个接口(默认使用 SimpleTriggerContext 实现)。下面列出了 Trigger 实现的可用方法。

public interface TriggerContext {

    Clock getClock();

    Instant lastScheduledExecution();

    Instant lastActualExecution();

    Instant lastCompletion();
}

5.2.2. Trigger 实现

Spring提供了两个 Trigger 接口的实现。最有趣的一个是 CronTrigger。它可以根据 cron表达式 来调度任务。例如,下面这个任务被安排在每小时15分钟后运行,但只在工作日的9点到5点的 "工作时间" 内运行:

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

另一个实现是一个 PeriodicTrigger,它接受一个固定的周期,一个可选的初始延迟值,以及一个布尔值来指示该周期是否应该被解释为固定速率或固定延迟。因为 TaskScheduler 接口已经定义了以固定速率或固定延迟调度任务的方法,所以只要有可能就应该直接使用这些方法。 PeriodicTrigger 实现的价值在于,你可以在依赖 Trigger 抽象的组件中使用它。例如,允许周期性触发器、基于cron的触发器,甚至是自定义触发器的实现交替使用,可能是很方便的。这样的组件可以利用依赖注入的优势,这样你就可以在外部配置这样的 Triggers,因此可以轻松地修改或扩展它们。

5.2.3. TaskScheduler 实现

与Spring的 TaskExecutor 抽象一样,TaskScheduler 安排的主要好处是,应用程序的调度需求与部署环境解耦。在部署到应用服务器环境中时,这种抽象层次尤其重要,因为在这种环境中线程不应该由应用本身直接创建。对于这种情况,Spring提供了一个 TimerManagerTaskScheduler,它可以委托给WebLogic或WebSphere上的CommonJ TimerManager,以及最近的 DefaultManagedTaskScheduler,在Jakarta EE环境中委托给JSR-236 ManagedScheduledExecutorService。两者通常都是通过 JNDI lookup 来配置的。

如果不需要外部线程管理,一个更简单的选择是在应用程序中设置本地的 ScheduledExecutorService,它可以通过 Spring 的 ConcurrentTaskScheduler 进行调整。为了方便起见,Spring还提供了一个 ThreadPoolTaskScheduler,它在内部委托给 ScheduledExecutorService,以提供与 ThreadPoolTaskExecutor 类似的普通bean式配置。在宽松的应用服务器环境中,这些变体对于本地嵌入的线程池设置也完全没有问题,特别是在Tomcat和Jetty上。

5.3. 对调度和异步执行的注解支持

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

5.3.1. 启用调度注解

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

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

你可以为你的应用程序挑选相关的注解。例如,如果你只需要对 @Scheduled 的支持,你可以省略 @EnableAsync。为了进行更精细的控制,你可以额外实现 SchedulingConfigurer 接口、AsyncConfigurer 接口或两者。请参阅 SchedulingConfigurerAsyncConfigurer javadoc 了解完整的细节。

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

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

注意,通过前面的XML,提供了一个 executor 引用,用于处理那些与带有 @Async 注解的方法相对应的任务,而提供了 scheduler 引用,用于管理那些带有 @Scheduled 注解的方法。

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

5.3.2. @Scheduled 注解

你可以给一个方法添加 @Scheduled 注解,以及触发器元数据。例如,下面这个方法每五秒(5000毫秒)被调用一次,有一个固定的延迟,也就是说,这个周期是从前面每次调用的完成时间开始计算的。

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

默认情况下,毫秒将被用作固定延迟、固定速率和初始延迟值的时间单位。如果你想使用一个不同的时间单位,如秒或分钟,你可以通过 @Scheduled 中的 timeUnit 属性进行配置。

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

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

如果你需要一个固定速率的执行,你可以在注解中使用 fixedRate 属性。下面的方法每五秒钟被调用一次(在每次调用的连续开始时间之间测量)。

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

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

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

如果简单的周期性调度的表现力不够,你可以提供一个 cron表达式。下面的例子只在工作日运行:

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

请注意,要被调度的方法必须是 void 的返回值,并且不接受任何参数。如果该方法需要与应用程序上下文中的其他对象进行交互,这些对象通常会通过依赖注入提供。

从 Spring Framework 4.3 开始,任何 scope 的Bean都支持 @Scheduled 方法。

请确保你在运行时不初始化同一个 @Scheduled 注解类的多个实例,除非你确实想对每个实例调度回调。与此相关,请确保你不在那些用 @Scheduled 注解并作为普通Spring Bean在容器中注册的bean类上使用 @Configurable。否则,你会得到双重初始化(一次通过容器,一次通过 @Configurable 切面),其结果是每个 @Scheduled 方法被调用两次。

5.3.3. @Async 注解

你可以在一个方法上提供 @Async 注解,这样该方法的调用就会以异步方式进行。换句话说,调用者在调用后立即返回,而该方法的实际执行发生在一个已经提交给 Spring TaskExecutor 的任务中。在最简单的情况下,你可以将注解应用于一个返回 void 的方法,就像下面的例子所示:

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

与带有 @Scheduled 注解的方法不同,这些方法可以期待参数,因为它们是由调用者在运行时以 "正常" 方式调用的,而不是由容器管理的 scheduled task 调用的。例如,下面的代码是 @Async 注解的一个合法应用:

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

即使是返回一个值的方法也可以异步调用。然而,这种方法需要有一个 Future 类型的返回值。这仍然提供了异步执行的好处,因此调用者可以在调用 Futureget() 之前执行其他任务。下面的例子显示了如何在一个返回值的方法上使用 @Async

@Async
Future<String> returnSomething(int i) {
    // this will be run asynchronously
}
@Async 方法不仅可以声明常规的 java.util.concurrent.Future 返回类型,还可以声明Spring的 org.springframework.util.concurrent.ListenableFuture,或者从Spring 4.2 开始,声明JDK 8的 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 AOP手动设置Spring的 AsyncExecutionInterceptor,并结合自定义的 pointcut。

5.3.4. 使用 @Async 的 Executor Qualification

默认情况下,当在一个方法上指定 @Async 时,所使用的 executor 是在 启用异步支持时配置 的 executor ,即 “annotation-driven” 元素(如果你使用XML)或你的 AsyncConfigurer 实现(如果有)。然而,当你需要表明在执行一个给定的方法时应该使用默认以外的 executor 时,你可以使用 @Async 注解的 value 属性。下面的例子展示了如何做到这一点:

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

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

5.3.5. 使用 @Async 的异常管理

当一个 @Async 方法有一个 Future 类型的返回值时,很容易管理方法执行过程中抛出的异常,因为这个异常是在对 Future 结果调用 get 时抛出的。然而,对于一个 void 的返回类型,该异常是未被捕获的,并且不能被传送。你可以提供一个 AsyncUncaughtExceptionHandler 来处理这种异常。下面的例子展示了如何做到这一点:

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

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

默认情况下,该异常只是被记录下来。你可以通过使用 AsyncConfigurer<task:annotation-driven/> XML元素定义一个自定义的 AsyncUncaughtExceptionHandler

5.4. task 命名空间

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

5.4.1. scheduler 元素

以下元素以指定的线程池大小创建了一个 ThreadPoolTaskScheduler 实例:

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

id 属性提供的值被用作池内线程名称的前缀。scheduler 元素是相对直接的。如果你没有提供一个 pool-size 的属性,默认的线程池只有一个线程。对于 scheduler 没有其他配置选项。

5.4.2. executor 元素

以下是创建一个 ThreadPoolTaskExecutor 实例:

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

上一节 所示的 scheduler 一样,为 id 属性提供的值被用作池内线程名称的前缀。就线程池的大小而言,executor 元素比 scheduler 元素支持更多的配置选项。首先,ThreadPoolTaskExecutor 的线程池本身是可配置的。一个 executor 的线程池可以有不同的 core size 和 max size 值,而不是只有一个值。如果你提供一个单一的值,executor 就有一个固定大小(fixed-size)的线程池(core 和 max 是一样的)。然而,executor 元素的 pool-size 属性也接受 min-max 形式的范围。下面的例子设置了一个最小值为 5,最大值为 25

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

在前面的配置中,还提供了一个 queue-capacity (队列容量)值。线程池的配置也应该根据 executor 的队列容量来考虑。关于线程池大小和队列容量之间关系的完整描述,请参阅 ThreadPoolExecutor 的文档。主要的思想是,当一个任务被提交时,如果当前活动线程的数量少于核心大小,executor 首先尝试使用一个空闲线程。如果已经达到了核心规模,任务就会被添加到队列中,只要其容量还没有达到。只有在这时,如果队列的容量已经达到,executor 才会创建一个超过核心规模的新线程。如果也达到了最大尺寸,那么 executor 就会拒绝该任务。

默认情况下,队列是无界的,但这很少是理想的配置,因为如果有足够的任务被添加到该队列,而所有的线程池线程都很忙的话,就会导致 OutOfMemoryErrors。此外,如果队列是无界的,最大尺寸根本没有影响。由于 executor 在创建超过核心大小的新线程之前总是会尝试队列,所以队列必须有一个有限的容量,以便线程池增长超过核心大小(这就是为什么在使用无界队列时,固定大小的池子是唯一明智的情况)。

考虑一下上面提到的情况,当一个任务被拒绝时。默认情况下,当一个任务被拒绝时,线程池执行器会抛出一个 TaskRejectedException。然而,拒绝策略实际上是可配置的。当使用默认的拒绝策略(即 AbortPolicy 的实现)时,会抛出该异常。对于在重负载下可以跳过一些任务的应用,你可以改成配置 DiscardPolicyDiscardOldestPolicy。对于需要在重负载下节制提交任务的应用来说,另一个很好的选择是 CallerRunsPolicy。该策略不是抛出一个异常或丢弃任务,而是强制调用提交方法的线程自己运行任务。其想法是,这样的调用者在运行该任务时很忙,无法立即提交其他任务。因此,它提供了一个简单的方法来节制传入的负载,同时保持线程池和队列的限制。通常情况下,这允许执行者 "赶上" 它正在处理的任务,从而在队列、线程池或两者中释放出一些容量。你可以从 executor 元素上的 rejection-policy 属性的列举值中选择任何这些选项。

下面的例子显示了一个带有许多属性的 executor 元素,用来指定各种行为:

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

最后,keep-alive 设置决定了线程在被停止之前可以保持空闲的时间限制(以秒计)。如果当前池中的线程数量超过了核心数量,那么在等待这个时间而不处理任务后,多余的线程会被停止。时间值为零会导致多余的线程在执行任务后立即停止,而不在任务队列中保留后续job。下面的例子将 keep-alive 值设置为两分钟:

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

5.4.3. scheduled-tasks 元素

Spring 的 task 命名空间最强大的功能是支持配置任务以在 Spring Application Context 中进行调度。这遵循了与Spring中其他 "方法调用者" 类似的方法,例如JMS命名空间为配置消息驱动的POJO所提供的方法。基本上,ref 属性可以指向任何Spring管理的对象,而 method 属性则提供了在该对象上调用的方法的名称。下面的列表显示了一个简单的例子:

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

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

scheduler 是由外层元素引用的,每个单独的任务包括其触发器元数据的配置。在前面的例子中,该元数据定义了一个具有固定延迟的周期性触发器,表示在每个任务执行完成后要等待的毫秒数。另一个选项是 fixed-rate,表示该方法应该多长时间运行一次,而不管之前的执行需要多长时间。此外,对于 fixed-delayfixed-rate 的任务,你可以指定一个 'initial-delay' 参数,表示方法第一次执行前要等待的毫秒数量。对于更多的控制,你可以改为提供一个 cron 属性来提供一个 cron表达式。下面的例子显示了这些其他选项:

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

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

5.5. Cron 表达式

All Spring cron expressions have to conform to the same format, whether you are using them in @Scheduled annotations, task:scheduled-tasks elements, or someplace else. A well-formed cron expression, such as * * * * * *, consists of six space-separated time and date fields, each with its own range of valid values:

所有的 Spring cron 表达式都必须符合相同的格式,无论你是在 @Scheduled 注解task:scheduled-tasks 元素,还是在其他地方使用它们。一个格式良好的cron表达式,如 * * * * * *,由六个空格分隔的时间和日期字段组成,每个字段有自己的有效值范围:

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

有一些规则是适用的:

  • 一个字段可以是一个星号(*),它总是代表 "首尾相连(first-last)"。对于月日或周日字段,可以用问号(?)代替星号。

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

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

  • 在一个范围(或 *)后面加上 /,指定数字的值在范围内的间隔。

  • 英文名称也可用于月和周的日期字段。使用特定日期或月份的前三个字母(大小写并不重要)。

  • 日和星期字段可以包含一个 L 字符,它有不同的含义。

    • 在日字段中,L 代表该月的最后一天。如果后面有一个负的偏移量(即 L-n),则表示该月的第 n 天到最后一天。

    • 在星期的字段中,L 代表一周的最后一天。如果前面有一个数字或三个字母的名称(dLDDDL),则表示的是该月的最后一个星期(dDDD)。

  • 日字段可以是 nW,它代表与本月 n 日最接近的工作日。如果 n 在星期六,这将产生它之前的星期五。如果 n 落在星期天,这将产生之后的星期一,如果 n1 并且落在星期六,这也会发生(也就是说:1W 代表每月的第一个工作日)。

  • 如果日字段为 LW,则表示该月的最后一个工作日。

  • 星期字段可以是 d#n(或 DDD#n),代表该月第 d 周(或 DDD)的第 n 天。

这里有一些例子:

Cron 表达式 含义

0 0 * * * *

每天每时每刻

*/10 * * * * *

每十秒钟

0 0 8-10 * * *

每天的8、9、10点钟

0 0 6,19 * * *

每天上午6:00和下午7:00

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

每月最后一天的12点

0 0 0 L-3 * *

倒数第三天的午夜时分

0 0 0 * * 5L

每月的最后一个星期五的午夜

0 0 0 * * THUL

每月最后一个星期四的午夜

0 0 0 1W * *

每月第一个工作日的12点

0 0 0 LW * *

每月最后一个工作日的午夜

0 0 0 ? * 5#2

每月第二个星期五的午夜

0 0 0 ? * MON#1

每月的第一个星期一的午夜

5.5.1. 宏

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

含义

@yearly (or @annually)

每年一次 (0 0 0 1 1 *)

@monthly

每月一次 (0 0 0 1 * *)

@weekly

每周一次 (0 0 0 * * 0)

@daily (or @midnight)

一天一次 (0 0 0 * * *)

@hourly

每小时一次 (0 0 * * * *)

5.6. 使用 Quartz Scheduler

Quartz 使用 TriggerJobJobDetail 对象来实现各种job的调度。关于 Quartz 背后的基本概念,见 Quartz网站。为了方便起见,Spring提供了几个类,以简化在基于 Spring 的应用程序中使用 Quartz。

5.6.1. 使用 JobDetailFactoryBean

Quartz JobDetail 对象包含了运行一个job所需的所有信息。Spring 提供了一个 JobDetailFactoryBean,它为XML配置目的提供了Bean式的属性。请看下面的例子:

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

job细节配置有运行job(ExampleJob)所需的所有信息。超时是在 job data map 中指定的。job data 是通过 JobExecutionContext(在执行时传递给你)获得的,但 JobDetail 也从 job data 到 job 实例的属性中获得其属性。所以,在下面的例子中,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.
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

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

你也可以使用 job data map 中的所有其他属性。

通过使用 namegroup 属性,你可以分别修改 job 的 name 和 group。默认情况下,job 的名称与 JobDetailFactoryBean 的Bean名称(上面的例子中的 exampleJob)一致。

5.6.2. 使用 MethodInvokingJobDetailFactoryBean

通常你只需要在一个特定的对象上调用一个方法。通过使用 MethodInvokingJobDetailFactoryBean,你完全可以做到这一点,如下例所示:

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

前面的例子导致 doIt 方法被调用到 exampleBusinessObject 方法上,如下图所示:

public class ExampleBusinessObject {

    // properties and collaborators

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

通过使用 MethodInvokingJobDetailFactoryBean,你不需要创建仅仅调用一个方法的单行作业。你只需要创建实际的业务对象和连接细节对象。

默认情况下,Quartz Job 是无状态的,导致 job 有可能相互干扰。如果你为同一个 JobDetail 指定了两个触发器,有可能第二个触发器在第一个job完成之前就开始了。如果 JobDetail 类实现了有 Stateful 接口,这种情况就不会发生:第二个 job 不会在第一个 job 完成之前开始。

为了使 MethodInvokingJobDetailFactoryBean 产生的 job 不具有并发性,请将 concurrent 标志设置为 false,如下例所示:

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="exampleBusinessObject"/>
    <property name="targetMethod" value="doIt"/>
    <property name="concurrent" value="false"/>
</bean>
默认情况下,job 将以并发的方式运行。

5.6.3. 通过使用 Trigger 和 SchedulerFactoryBean 来连接 Job

我们已经创建了 job detail 和 job。我们还回顾了便利bean,它可以让你在一个特定的对象上调用一个方法。当然,我们仍然需要调度 job 本身。这可以通过使用触发器和 SchedulerFactoryBean 来完成。在Quartz中,有几个触发器是可用的,Spring 提供了两个Quartz FactoryBean 的实现,具有方便的默认值: CronTriggerFactoryBeanSimpleTriggerFactoryBean

触发器需要被调度。Spring提供了一个 SchedulerFactoryBean,它公开了可以作为属性设置的触发器(trigger)。SchedulerFactoryBean 用这些触发器来调度实际作业。

下面的清单同时使用了一个 SimpleTriggerFactoryBean 和一个 CronTriggerFactoryBean

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

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

前面的例子设置了两个触发器,一个每50秒运行一次,起始延迟为10秒,另一个每天早上6点运行。为了最终完成一切,我们需要设置 SchedulerFactoryBean,如下例所示:

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="cronTrigger"/>
            <ref bean="simpleTrigger"/>
        </list>
    </property>
</bean>

SchedulerFactoryBean 有更多的属性,比如 job detail 所使用的 calendar、用来定制Quartz的属性以及Spring提供的 JDBC DataSource。参见 SchedulerFactoryBean javadoc 以了解更多信息。

SchedulerFactoryBean 还能识别 classpath 中的 quartz.properties 文件,基于 Quartz property key,与常规 Quartz 配置一样。请注意,许多 SchedulerFactoryBean 设置与 properties 文件中的普通Quartz设置相互作用;因此不建议在两个层面上都指定值。例如,如果你想依靠Spring提供的数据源,就不要设置 "org.quartz.jobStore.class" 属性,或者指定 org.springframework.scheduling.quartz.LocalDataSourceJobStore 变体,它是标准 org.quartz.impl.jdbcjobstore.JobStoreTX 的全面替代。

6. 缓存抽象

从 3.1 版本开始,Spring 框架提供了对现有 Spring 应用透明地添加缓存的支持。与 事务 支持类似,缓存抽象允许一致使用各种缓存解决方案,对代码的影响最小。

在 Spring Framework 4.1 中,缓存抽象得到了极大的扩展,支持 JSR-107 注解 和更多的自定义选项。

6.1. 了解缓存抽象

Cache 和 Buffer

术语,“buffer” (缓冲区)和 “cache”(缓存),往往被交替使用。然而,请注意,它们代表的是不同的东西。传统上,缓冲区(buffer)被用作快速和慢速实体之间数据的中间临时存储。由于一方必须等待另一方(这影响了性能),缓冲区通过允许整个数据块一次性移动而不是小块移动来缓解这一问题。数据只需从缓冲区写入和读取一次。此外,缓冲区至少对知道它的一方是可见的。

另一方面,根据定义,缓存是隐藏的,任何一方都不会意识到缓存的存在。它也能提高性能,但它是通过让同一数据被快速读取多次来实现的。

你可以在 这里 找到关于缓冲区和缓存之间的区别的进一步解释。

缓存抽象的核心是对Java方法进行缓存,从而根据缓存中的可用信息减少执行次数。也就是说,每当一个目标方法被调用时,该抽象都会应用一个缓存行为,检查该方法是否已经被给定参数调用过。如果它已经被调用,缓存的结果将被返回,而不需要调用实际的方法。如果该方法没有被调用,那么它就被调用,结果被缓存并返回给用户,这样,在下一次调用该方法时,就会返回缓存的结果。这样一来,昂贵的方法(无论是CPU还是IO-bound)对于一组给定的参数可以只被调用一次,并且结果可以重复使用,而不需要再次实际调用该方法。缓存逻辑是透明地应用的,对调用者没有任何干扰。

这种方法只适用于那些无论调用多少次都能保证对给定输入(或参数)返回相同输出(结果)的方法。

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

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

Spring 提供了 一些该抽象的实现: 基于 JDK java.util.concurrent.ConcurrentMap 的缓存、Gemfire 缓存、 Caffeine 以及符合JSR-107标准的缓存(如 Ehcache 3.x)。参见 插入不同的后端缓存 ,以获得更多关于插入其他缓存存储和提供者的信息。

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

如果你有一个多进程的环境(也就是一个部署在几个节点上的应用程序),你需要相应地配置你的缓存提供者。根据你的使用情况,在几个节点上有一份相同的数据拷贝就足够了。但是,如果你在应用过程中改变了数据,你可能需要启用其他传播机制。

缓存一个特定的项目直接等同于典型的 get-if-not-found-then-proceed-and-put-eventually 代码块,在程序化的缓存交互中发现。没有加锁,几个线程可以尝试同时加载同一个项目。这同样适用于驱逐。如果几个线程试图同时更新或驱逐数据,你可能会读取到旧的数据。某些缓存提供者在这方面提供了高级功能。更多细节请参见缓存提供者的文档。

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

  • 缓存声明: 确定需要缓存的方法和它们的策略。

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

6.2. 基于声明式注解的缓存

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

  • @Cacheable: 触发缓存的填充。

  • @CacheEvict: 触发缓存驱逐。

  • @CachePut: 更新缓存而不干扰方法的执行。

  • @Caching: 将多个缓存操作重新分组,应用在一个方法上。

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

6.2.1. @Cacheable 注解

顾名思义,你可以使用 @Cacheable 来划分可缓存的方法—​也就是那些结果被存储在缓存中的方法,这样,在后续的调用中(使用相同的参数),缓存中的值就会被返回,而无需实际调用该方法。在其最简单的形式中,注解声明需要与被注解方法相关的缓存的名称,正如下面的例子所示:

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

在前面的片段中,findBook 方法与名为 books 的缓存相关。每次调用该方法时,都会检查缓存,看是否已经运行过该调用,而不必重复。虽然在大多数情况下,只有一个缓存被声明,但注解允许指定多个名字,这样就可以使用多个缓存了。在这种情况下,每个缓存在调用方法之前都会被检查—​如果至少有一个缓存被命中,相关的值会被返回。

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

下面的例子在有多个缓存的 findBook 方法上使用了 @Cacheable

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

由于缓存本质上是键值存储,每一次对缓存方法的调用都需要转化为一个合适的键(key),以便缓存访问。缓存抽象使用一个简单的基于以下算法的 KeyGenerator

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

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

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

只要参数有自然的key,并且实现了有效的 hashCode()equals() 方法,这种方法对大多数使用情况都很有效。如果不是这种情况,你需要改变策略。

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

默认的 key 生成策略随着Spring 4.0的发布而改变。早期版本的Spring使用了一种 key 生成策略,对于多个 key 参数,只考虑参数的 hashCode() 而不是 equals()。这可能会导致意外的 key 碰撞(背景见 SPR-10237)。新的 SimpleKeyGenerator 在这种情况下使用一个复合 key。

如果你想继续使用之前的 key 策略,你可以配置已废弃的 org.springframework.cache.interceptor.DefaultKeyGenerator 类,或者创建一个自定义的基于哈希的 KeyGenerator 实现。

自定义 key 生成声明

由于缓存是通用的,目标方法很可能有各种签名,而这些签名不能轻易地映射到缓存结构之上。当目标方法有多个参数,而其中只有一些适合缓存(其余的只用于方法逻辑)时,这种情况就变得很明显。请看下面的例子:

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

乍一看,虽然这两个 boolean 参数影响了 book 的查找方式,但它们对缓存没有用处。此外,如果这两个参数中只有一个是重要的,而另一个不重要呢?

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

下面的例子使用了各种SpEL声明(如果你不熟悉SpEL,自己动手,读一下 Spring Expression Language):

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

前面的片段显示了选择某个参数、它的一个属性、甚至是一个任意的(静态)方法是多么容易。

如果负责生成 key 的算法过于特殊或需要共享,你可以在操作上定义一个自定义的 keyGenerator。要做到这一点,指定要使用的 KeyGenerator Bean实现的名称,如下例所示:

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
keykeyGenerator 参数是相互排斥的,同时指定这两个参数的操作会产生一个异常。
默认的缓存解析

缓存抽象使用一个简单的 CacheResolver,通过使用配置的 CacheManager 检索在操作层定义的缓存。

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

自定义缓存解析

默认的缓存解析很适合那些使用单个 CacheManager 并且没有复杂缓存解析要求的应用。

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

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

你也可以用类似于替换 key generation 的方式来完全替换 CacheResolver。每一次缓存操作都会请求解析,让实现根据运行时的参数实际解析要使用的缓存。下面的例子展示了如何指定一个 CacheResolver

@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
1 指定 CacheResolver

从 Spring 4.1 开始,缓存注解的 value 属性不再是强制性的,因为无论注解的内容如何,CacheResolver 都可以提供这一特殊信息。

keykeyGenerator 类似,cacheManagercacheResolver 参数是相互排斥的,同时指定这两个参数的操作会导致一个异常,因为 CacheResolver 的实现会忽略自定义的 CacheManager。这可能不是你所期望的。

同步缓存

在一个多线程的环境中,某些操作可能会对同一个参数进行并发调用(通常是在启动时)。默认情况下,缓存抽象并不对任何东西加锁,同一个值可能会被计算好几次,这就违背了缓存的目的。

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

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

有时,一个方法可能不适合一直被缓存(例如,它可能取决于给定的参数)。缓存注解通过 condition 参数支持这样的用例,condition 参数接受一个SpEL表达式,该表达式被评估为 truefalse。如果为 true,该方法被缓存。如果不是,它的行为就像该方法没有被缓存一样(也就是说,无论缓存中的值是什么或者使用了什么参数,该方法每次都被调用)。例如,只有当参数 name 的长度(length)短于 32 时,下面的方法才被缓存:

@Cacheable(cacheNames="book", condition="#name.length() < 32") (1)
public Book findBook(String name)
1 @Cacheable 上设置一个条件。

除了 condition 参数外,你还可以使用 unless 参数来否决向缓存中添加值。与 condition 不同的是,unless 表达式是在方法被调用后进行评估的。为了扩展前面的例子,也许我们只想缓 paperback book,就像下面的例子那样:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1)
public Book findBook(String name)
1 使用 unless 属性来阻止 hardback。

缓存抽象支持 java.util.Optional 返回类型。如果一个 Optional 值存在,它将被存储在相关的缓存中。如果一个 Optional 值不存在,null 将被存储在相关的缓存中。#result 总是指业务实体,而不是支持的 wrapper,所以前面的例子可以改写成如下:

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

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

可用的缓存 SpEL Evaluation Context

Each SpEL expression evaluates against a dedicated context. In addition to the built-in parameters, the framework provides dedicated caching-related metadata, such as the argument names. The following table describes the items made available to the context so that you can use them for key and conditional computations:

每个 SpEL 表达式都针对一个专门的 context 进行评估。除了内置参数外,框架还提供了专门的缓存相关元数据,如参数名称等。下表描述了提供给 context 的项目,以便你可以使用它们进行 key 和条件计算:

Table 9. 缓存SpEL可用的元数据
名称 定位 说明 示例

methodName

Root 对象

被调用的方法的名称

#root.methodName

method

Root 对象

被调用的方法

#root.method.name

target

Root 对象

被调用的目标对象

#root.target

targetClass

Root 对象

被调用的目标的类

#root.targetClass

args

Root 对象

用于调用目标的参数(以数组形式)。

#root.args[0]

caches

Root 对象

运行当前方法所针对的缓存的集合

#root.caches[0].name

参数名称

Evaluation context

任何一个方法参数的名称。如果名称不可用(也许是由于没有 debug 信息),参数名称也可以在 #a<#arg> 下获得,其中 #arg 代表参数索引(从 0 开始)。

#iban or #a0(你也可以用 #p0#p<#arg> 的符号作为别名)。

result

Evaluation context

方法调用的结果(要被缓存的值)。只在 unless 表达式、cache put 表达式(计算 key)或 cache evict 表达式(当 beforeInvocationfalse 时)中可用。对于支持的 wrapper(如 Optional),#result 指的是实际对象,而不是 wrapper(如。

#result

6.2.2. @CachePut 注解

当需要更新缓存而不干扰方法的执行时,你可以使用 @CachePut 注解。也就是说,该方法总是被调用,其结果被放入缓存(根据 @CachePut 选项)。它支持与 @Cacheable 相同的选项,应该用于缓存的填充而不是方法流的优化。下面的例子使用了 @CachePut 注解:

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
在同一个方法上使用 @CachePut@Cacheable 注解通常是非常不可取的,因为它们有不同的行为。后者会使方法的调用因使用缓存而被跳过,而前者则是为了运行缓存更新而强制调用。这导致了意想不到的行为,除了特定的角落案例(比如注解中的条件相互排斥),这种声明应该被避免。还要注意的是,这样的条件不应该依赖于结果对象(也就是 #result 变量),因为这些都是预先验证过的,以确认排除的情况。

6.2.3. @CacheEvict 注解

缓存抽象不仅允许缓存存储,而且还允许驱逐。这个过程对于从缓存中移除陈旧或未使用的数据很有用。与 @Cacheable 相反,@CacheEvict 划分了执行缓存驱逐的方法(也就是说,作为从缓存中移除数据的触发器的方法)。与它的兄弟姐妹类似,@CacheEvict 需要指定一个或多个受行动影响的缓存,允许指定一个自定义的缓存和 key 解析或条件,并具有一个额外的参数(allEntries),表明是否需要执行整个缓存的驱逐,而不仅仅是一个条目的驱逐(基于 key)。下面的例子从 books 缓存中驱逐了所有条目:

@CacheEvict(cacheNames="books", allEntries=true) (1)
public void loadBooks(InputStream batch)
1 使用 allEntries 属性来驱逐缓存中的所有条目。

当整个缓存区域需要被清除时,这个选项就派上用场了。与其逐出每个条目(这将花费很长的时间,因为它的效率很低),不如在一次操作中删除所有的条目,如前面的例子所示。注意,框架忽略了在这种情况下指定的任何 key,因为它并不适用(整个缓存被驱逐,而不是只有一个条目)。

你也可以通过使用 beforeInvocation 属性来表明驱逐应该发生在方法被调用之后(默认)还是之前。前者提供了与其他注解相同的语义: 一旦该方法成功完成,缓存上的一个动作(在这里是驱逐)就会被运行。如果该方法没有运行(因为它可能被缓存了)或者被抛出一个异常,那么驱逐就不会发生。后者( beforeInvocation=true)导致驱逐总是在方法被调用之前发生。这在驱逐不需要与方法结果相联系的情况下很有用。

请注意,void 方法可以和 @CacheEvict 一起使用—​因为这些方法作为一个触发器,其返回值被忽略(因为它们不与缓存交互)。而 @Cacheable 则不是这样,它向缓存中添加数据或更新缓存中的数据,因此需要一个结果。

6.2.4. @Caching 注解

有时,同一类型的多个注解(如 @CacheEvict@CachePut)需要被指定—​例如,因为不同缓存的 condition 或 key 表达式是不同的。@Caching 允许在同一个方法上使用多个嵌套的 @Cacheable@CachePut@CacheEvict 注解。下面的例子使用了两个 @CacheEvict 注解:

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

6.2.5. @CacheConfig 注解

到目前为止,我们已经看到缓存操作提供了许多自定义选项,而且你可以为每个操作设置这些选项。然而,如果一些自定义选项适用于类的所有操作,那么配置起来就会很繁琐。例如,为类的每个缓存操作指定使用的缓存名称,可以用一个类级的定义来代替。这就是 @CacheConfig 开始发挥作用的地方。下面的例子使用 @CacheConfig 来设置缓存的名称:

@CacheConfig("books") (1)
public class BookRepositoryImpl implements BookRepository {

    @Cacheable
    public Book findBook(ISBN isbn) {...}
}
1 使用 @CacheConfig 来设置缓存的名称。

@CacheConfig 是一个类级注解,它允许共享缓存名称、自定义 KeyGenerator、自定义 CacheManager 和自定义 CacheResolver。把这个注解放在类上并不开启任何缓存操作。

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

  • 全局配置,可用于 CacheManagerKeyGenerator

  • 在类级别,使用 @CacheConfig

  • 在操作级别上。

6.2.6. 启用缓存注解

需要注意的是,即使声明了缓存注解,也不会自动触发它们的动作—​就像Spring中的许多东西一样,这个功能必须要声明启用(这意味着如果你怀疑缓存是罪魁祸首,你可以只通过删除一个配置行而不是代码中的所有注解来禁用它)。

要启用缓存注解,请将注解 @EnableCaching 添加到你的一个 @Configuration 类中:

@Configuration
@EnableCaching
public class AppConfig {
}

另外,对于XML配置,你可以使用 cache:annotation-driven 元素:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">

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

cache:annotation-driven 元素和 @EnableCaching 注解都可以让你指定各种选项,影响通过AOP添加到应用程序的缓存行为的方式。该配置有意与 @Transactional 的配置相似。

处理缓存注解的默认 advice mode 是 proxy,它只允许通过代理拦截调用。同一类中的本地调用不能通过这种方式被拦截。对于更高级的拦截模式,可以考虑切换到 aspectj mode,结合编译时或加载时织入。
关于实现 CachingConfigurer 所需的高级定制(使用Java配置)的更多细节,请参阅 javadoc
Table 10. 缓存注解设置
XML 属性 注解属性 默认 说明

cache-manager

N/A (见 CachingConfigurer javadoc)

cacheManager

要使用的缓存管理器的名称。默认的 CacheResolver 会在幕后与这个缓存管理器一起被初始化(如果没有设置 cacheManager)。如果要对缓存解析进行更精细的管理,可以考虑设置 'cache-resolver' 属性。

cache-resolver

N/A (见 the CachingConfigurer javadoc)

一个使用配置的 cacheManagerSimpleCacheResolver

CacheResolver 的Bean名称,它将被用于解析后备缓存。这个属性不是必须的,只需要作为 'cache-manager' 属性的替代品来指定。

key-generator

N/A (见 the CachingConfigurer javadoc)

SimpleKeyGenerator

要使用的自定义 key generator 的名称。

error-handler

N/A (见 the CachingConfigurer javadoc)

SimpleCacheErrorHandler

要使用的自定义缓存 cache error handler 的名称。默认情况下,任何在缓存相关操作中抛出的异常都会被抛回给客户端。

mode

mode

proxy

默认 mode(proxy)通过使用Spring的AOP框架(遵循代理语义,如前所述,仅适用于通过代理进来的方法调用)来处理要代理的注解bean。另一种 mode(aspectj)则是用Spring的AspectJ缓存切面来织入受影响的类,修改目标类的字节码以适用于任何类型的方法调用。AspectJ织入需要在classpath中加入 spring-aspects.jar,并启用加载时编织(或编译时编织)。(参见 Spring配置,了解如何设置加载时织入的细节)。

proxy-target-class

proxyTargetClass

false

仅适用于 proxy mode。控制为带有 @Cacheable@CacheEvict 注解的类创建何种类型的缓存代理。如果 proxy-target-class 属性被设置为 true,就会创建基于类的代理。如果 proxy-target-classfalse,或者该属性被省略,将创建基于JDK接口的标准代理。(参见 代理机制 以详细了解不同的代理类型)。

order

order

Ordered.LOWEST_PRECEDENCE

定义缓存 advice 的顺序,这些 advice 被应用到用 @Cacheable@CacheEvict 注解的Bean上。(关于AOP advice 的排序规则的更多信息,请参见 Advice 顺序)。没有指定排序意味着AOP子系统决定 advice 的顺序。

<cache:annotation-driven/> 只在定义它的同一 application context 中的bean上寻找 @Cacheable/@CachePut/@CacheEvict/@Caching。这意味着,如果你把 <cache:annotation-driven/> 放在一个 DispatcherServletWebApplicationContext 中,它只检查 controller 中的 Bean,而不是 service。更多信息请参见 MVC部分
方法的可见性和缓存注解

当你使用代理时,你应该只对具有 public 可见性的方法应用缓存注解。如果你确实用这些注解来注解 protectedprivate 的或包可见的方法,就不会产生错误,但被注解的方法不会表现出配置的缓存设置(没有缓存效果)。如果你需要注解非 public 方法,请考虑使用AspectJ(见本节的其余部分),因为它改变了字节码本身。

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

6.2.7. 使用自定义注解

自定义注解和AspectJ

这个功能只适用于基于 proxy 的方法,但通过使用AspectJ,可以用一点额外的努力来启用。

spring-aspects 模块只为标准注解定义了一个切面。如果你定义了自己的注解,你也需要为这些注解定义一个方面。请看 AnnotationCacheAspect 的例子。

缓存抽象让你使用自己的注解来识别什么方法会触发缓存存储或驱逐。这作为一种模板机制是相当方便的,因为它省去了重复的缓存注解声明,如果指定了 key 或条件(condition),或者在你的代码库中不允许使用外来的导入(org.springframework),这就特别有用。与其他的 stereotype 注解类似,你可以将 @Cacheable@CachePut@CacheEvict@CacheConfig 作为 元注解(也就是可以注解其他注解的注解)。在下面的例子中,我们用我们自己的自定义注解替换了一个普通的 @Cacheable 声明:

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

在前面的例子中,我们已经定义了我们自己的 SlowService 注解,它本身就是用 @Cacheable 来注解的。现在我们可以替换下面的代码:

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

下面的例子显示了自定义注解,我们可以用它来替换前面的代码:

@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

即使 @SlowService 不是Spring注解,容器也会在运行时自动接收其声明并理解其含义。注意,如 所述,需要启用注解驱动的行为。

6.3. JCache (JSR-107) 注解

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

6.3.1. 特性总结

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

Table 11. 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 检索的。可以为每个缓存操作定制 factory,如下例所示:

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

key 是由 javax.cache.annotation.CacheKeyGenerator 生成的,其作用与 Spring 的 KeyGenerator 相同。默认情况下,所有的方法参数都被考虑在内,除非至少有一个参数被 @CacheKey 注解。这与 Spring 的 自定义 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。这是一个永久性的失败(没有 book 可以用这样的参数被检索到)。下面的方法会缓存这个异常,这样以后再调用相同的、无效的ISBN时,就会直接抛出缓存的异常,而不是再次调用这个方法:

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

6.3.2. 启用JSR-107支持

你不需要做任何特别的事情来启用JSR-107支持和Spring的声明式注解支持。如果JSR-107 API和 spring-context-support 模块都在classpath中,@EnableCachingcache:annotation-driven XML元素都会自动启用 JCache 支持。

根据你的使用情况,选择权基本上在你手中。你甚至可以通过在一些服务上使用JSR-107 API,在其他服务上使用Spring自己的注解来混合和匹配服务。然而,如果这些服务影响到相同的缓存,你应该使用一致的、相同的 key generation 实现。

6.4. 基于 XML 的声明式缓存

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

<!-- the service we want to make cacheable -->
<bean id="bookService" class="x.y.service.DefaultBookService"/>

<!-- cache definitions -->
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
    <cache:caching cache="books">
        <cache:cacheable method="findBook" key="#isbn"/>
        <cache:cache-evict method="loadBooks" all-entries="true"/>
    </cache:caching>
</cache:advice>

<!-- apply the cacheable behavior to all BookService interfaces -->
<aop:config>
    <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>

<!-- cache manager definition omitted -->

在前面的配置中,bookService 是可缓存的。应用的缓存语义被封装在 cache:advice 定义中,它导致 findBooks 方法被用于将数据放入缓存,loadBooks 方法被用于驱逐数据。这两个定义都是针对 books 缓存工作的。

aop:config 定义通过使用 AspectJ 的 pointcut 表达式将 cache advice 应用到程序中的适当位置(更多信息请参见 使用Spring的面向切面的编程)。在前面的例子中,BookService 的所有方法都被考虑到了,cache advice 也被应用到了这些方法中。

声明式XML缓存支持所有基于注解的模型,所以在这两者之间移动应该是相当容易的。此外,两者都可以在同一个应用程序中使用。基于XML的方法不接触目标代码。然而,它在本质上是更加啰嗦的。当处理有重载方法的类作为缓存的目标时,识别适当的方法确实需要额外的努力,因为 method 参数不是一个很好的判别标准。在这种情况下,你可以使用 AspectJ 的 pointcut 来挑选目标方法,并应用适当的缓存功能。然而,通过XML,应用包或组或 interface 范围内的缓存(同样,由于 AspectJ pointcut)和创建类似模板的定义(就像我们在前面的例子中通过 cache:definitions cache 属性定义目标缓存那样)更容易。

6.5. 配置缓存存储

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

6.5.1. 基于JDK ConcurrentMap 的缓存

基于JDK的 Cache 实现位于 org.springframework.cache.concurrent 包中。它可以让你使用 ConcurrentHashMap 作为 Cache 的支持存储。下面的例子展示了如何配置两个缓存:

<!-- simple cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
    <property name="caches">
        <set>
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
        </set>
    </property>
</bean>

前面的片段使用 SimpleCacheManager 为两个嵌套的 ConcurrentMapCache 实例创建一个 CacheManager,命名为 defaultbooks。注意,这些名字是直接为每个缓存配置的。

由于缓存是由应用程序创建的,它被绑定在其生命周期中,使得它适用于基本用例、测试或简单的应用程序。缓存的扩展性很好,速度也非常快,但它不提供任何管理、持久化能力或驱逐契约。

6.5.2. 基于 Ehcache 的缓存

Ehcache 3.x完全符合JSR-107标准,不需要对它进行专门支持。详见 JSR-107 缓存

6.5.3. Caffeine 缓存

Caffeine是Guava缓存的Java 8重写,其实现位于 org.springframework.cache.caffeine 包中,并提供了对 Caffeine 若干特性的访问。

下面的例子配置了一个按需创建缓存的 CacheManager

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

你也可以明确地提供要使用的缓存。在这种情况下,只有那些缓存是由管理器提供的。下面的例子显示了如何做到这一点:

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

Caffeine CacheManager 也支持自定义的 CaffeineCacheLoader。有关这些的更多信息,请参见 Caffeine文档

6.5.4. 基于 GemFire 的缓存

GemFire 是一个面向内存的、有磁盘支持的、可弹性扩展的、连续可用的、主动的(内置基于模式的订阅通知)、全局复制的数据库,并提供功能齐全的边缘缓存。关于如何使用 GemFire 作为 CacheManager(以及更多)的进一步信息,请参见 Spring Data GemFire 参考文档

6.5.5. JSR-107 缓存

Spring的缓存抽象也可以使用符合JSR-107标准的缓存。JCache的实现位于 org.springframework.cache.jcache 包中。

同样,为了使用它,你需要声明适当的 CacheManager。下面的例子显示了如何做到这一点:

<bean id="cacheManager"
        class="org.springframework.cache.jcache.JCacheCacheManager"
        p:cache-manager-ref="jCacheManager"/>

<!-- JSR-107 cache manager setup  -->
<bean id="jCacheManager" .../>

6.5.6. 处理没有后备存储的缓存

有时,在切换环境或进行测试时,你可能有缓存声明,但没有配置实际的支持缓存。由于这是一个无效的配置,在运行时就会抛出一个异常,因为缓存基础设施无法找到一个合适的存储。在这种情况下,与其删除缓存声明(这可能会很繁琐),你可以在一个简单的虚拟缓存中不执行缓存—​也就是说,它强制每次都调用缓存方法。下面的例子展示了如何做到这一点:

<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
    <property name="cacheManagers">
        <list>
            <ref bean="jdkCache"/>
            <ref bean="gemfireCache"/>
        </list>
    </property>
    <property name="fallbackToNoOpCache" value="true"/>
</bean>

前面的 CompositeCacheManager 将多个 CacheManager 实例连接起来,并通过 fallbackToNoOpCache 标志,为所有未被配置的缓存管理器处理的定义添加一个 no-op 缓存。也就是说,每一个在 jdkCachegemfireCache(在本例前面配置的)中没有找到的缓存定义都由 no-op 缓存处理,它不存储任何信息,导致目标方法每次都被调用。

6.6. 插入不同的后端缓存

很明显,有很多缓存产品可以作为后备存储使用。对于那些不支持JSR-107的产品,你需要提供一个 CacheManager 和一个 Cache 实现。这听起来可能比实际要难,因为在实践中,这些类往往是简单的 适配器,将缓存抽象框架映射到存储API之上,就像 Caffeine 类那样。大多数 CacheManager 类可以使用 org.springframework.cache.support 包中的类(比如 AbstractCacheManager,它负责处理模板代码,只留下实际的映射需要完成)。

6.7. 如何设置 TTL/TTI/Eviction 策略/XXX功能?

直接通过你的缓存提供者。缓存抽象是一个抽象,而不是一个缓存实现。你使用的解决方案可能支持各种数据策略和其他解决方案不支持的不同拓扑结构(例如,JDK ConcurrentHashMap - 在缓存抽象中暴露出来将是无用的,因为没有支持)。这样的功能应该直接通过支持的缓存来控制(在配置它的时候)或者通过它的本地API。

7. 可观测性支持

Micrometer 定义了一个 观察概念,可以在应用程序中实现度量和跟踪。Metrics支持提供了一种创建计时器、仪表或计数器的方法,用于收集关于你的应用程序的运行时行为的统计数据。度量标准可以帮助你跟踪错误率、使用模式、性能等。追踪提供了整个系统的整体视图,跨越了应用程序的边界;你可以放大特定的用户请求,并跟踪它们在应用程序中的整个完成情况。

如果配置了 ObservationRegistry,Spring 框架就会利用其自身代码库的各个部分来发布观察结果。你可以了解更多关于 配置 Spring Boot中的可观察性基础设施的信息。

7.1. 产生的观测结果列表

Spring Framework为可观测性提供了各种功能。正如 本节开头 所概述的,根据配置,观察可以产生 timer 指标和/或 Trace。

Table 12. 由Spring Framework产生的观测结果
观测名称 说明

"http.client.requests"

HTTP client exchange 所花费的时间

"http.server.requests"

框架层面上的 HTTP server exchange 的处理时间

观测结果使用 Micrometer 的官方命名规则,但 Metric 的名称将 自动转换为监测系统后台(Prometheus、Atlas、Graphite、InfluxDB…​)所喜欢的格式。

7.2. Micrometer Observation 概念

如果你不熟悉 Micrometer Observation,这里有一个你应该知道的新概念的快速总结。

  • Observation 是对你的应用程序中发生的事情的实际记录。它由 ObservationHandler 实现处理,产生指标或追踪(trace)。

  • 每个 observation 都有一个相应的 ObservationContext 实现;这种类型持有所有相关的信息,用于提取它的元数据。在HTTP服务器 observation 的情况下,上下文的实现可以持有HTTP请求、HTTP响应、处理过程中抛出的任何异常…​…​

  • 每个 Observation 都持有 KeyValues 元数据。在 server HTTP observation 的情况下,这可能是HTTP请求方法、HTTP响应状态…​…​这些元数据由 ObservationConvention 实现提供,它应该声明它们支持的 ObservationContex t类型。

  • 如果 KeyValue 元组的可能值数量较少且有限制,那么 KeyValues 就被称为 "low cardinality"(HTTP方法就是一个很好的例子)。Low cardinality 值只贡献给度量。另一方面,High cardinality 值是无界的(例如,HTTP请求URI),只贡献给 Traces。

  • 一个 ObservationDocumentation 文档,记录了特定 domain 的所有 observation,列出了预期的 key 名称和它们的含义。

7.3. 配置 Observation

全局配置选项在 ObservationRegistry#observationConfig() 级别上可用。每个仪表化的组件将提供两个扩展点:

  • 设置 ObservationRegistry;如果没有设置,观察结果将不会被记录,并且是无用的。

  • 提供一个自定义的 ObservationConvention 来改变默认的 observation 名称和提取的 KeyValues

7.3.1. 使用自定义的 Observation convention

让我们以Spring MVC的 "http.server.requests" 度量仪表与 ServerHttpObservationFilter 为例。这个 observation 是使用 ServerRequestObservationConventionServerRequestObservationContext;自定义 conventions可以在Servlet过滤器上配置。如果你想自定义随 observation 结果产生的元数据,你可以根据你的要求继承 DefaultServerRequestObservationConvention

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;

import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;

public class ExtendedServerRequestObservationConvention extends DefaultServerRequestObservationConvention {

    @Override
    public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
        // here, we just want to have an additional KeyValue to the observation, keeping the default values
        return super.getLowCardinalityKeyValues(context).and(custom(context));
    }

    private KeyValue custom(ServerRequestObservationContext context) {
        return KeyValue.of("custom.method", context.getCarrier().getMethod());
    }

}

如果你想完全控制,你就可以为你感兴趣的 observation 对象实施整个 onvention contract:

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;

import org.springframework.http.server.observation.ServerHttpObservationDocumentation;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.http.server.observation.ServerRequestObservationConvention;

public class CustomServerRequestObservationConvention implements ServerRequestObservationConvention {

    @Override
    public String getName() {
        // will be used as the metric name
        return "http.server.requests";
    }

    @Override
    public String getContextualName(ServerRequestObservationContext context) {
        // will be used for the trace name
        return "http " + context.getCarrier().getMethod().toLowerCase();
    }

    @Override
    public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
        return KeyValues.of(method(context), status(context), exception(context));
    }


    @Override
    public KeyValues getHighCardinalityKeyValues(ServerRequestObservationContext context) {
        return KeyValues.of(httpUrl(context));
    }

    private KeyValue method(ServerRequestObservationContext context) {
        // You should reuse as much as possible the corresponding ObservationDocumentation for key names
        return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod());
    }

    // status(), exception(), httpUrl()...

    private KeyValue status(ServerRequestObservationContext context) {
        return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, String.valueOf(context.getResponse().getStatus()));
    }

    private KeyValue exception(ServerRequestObservationContext context) {
        String exception = (context.getError() != null) ? context.getError().getClass().getSimpleName() : KeyValue.NONE_VALUE;
        return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, exception);
    }

    private KeyValue httpUrl(ServerRequestObservationContext context) {
        return KeyValue.of(ServerHttpObservationDocumentation.HighCardinalityKeyNames.HTTP_URL, context.getCarrier().getRequestURI());
    }

}

你也可以使用自定义的 ObservationFilter 来实现类似的目标—​增加或删除 observation 的 key value。过滤器并不取代默认的 convention,而是作为一个后处理组件使用。

import io.micrometer.common.KeyValue;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationFilter;

import org.springframework.http.server.observation.ServerRequestObservationContext;

public class ServerRequestObservationFilter implements ObservationFilter {

    @Override
    public Observation.Context map(Observation.Context context) {
        if (context instanceof ServerRequestObservationContext serverContext) {
            context.setName("custom.observation.name");
            context.addLowCardinalityKeyValue(KeyValue.of("project", "spring"));
            String customAttribute = (String) serverContext.getCarrier().getAttribute("customAttribute");
            context.addLowCardinalityKeyValue(KeyValue.of("custom.attribute", customAttribute));
        }
        return context;
    }
}

你可以在 ObservationRegistry 上配置 ObservationFilter 实例。

7.4. HTTP Server 仪表

对于Servlet和Reactive应用程序,HTTP server exchange observation 是以 "http.server.requests" 为名创建的。

7.4.1. Servlet 应用

应用程序需要在其应用程序中配置 org.springframework.web.filter.ServerHttpObservationFilter Servlet过滤器。它默认使用 org.springframework.http.server.observation.DefaultServerRequestObservationConvention,由 ServerRequestObservationContext 支持。

只有当 Exception 没有被Web框架处理,并上升到Servlet过滤器时,这才会将 observation 记录为错误。通常情况下,所有由Spring MVC的 @ExceptionHandlerProblemDetail 支持 处理的异常都不会被记录在 observation 中。你可以在请求处理的任何时候,自己在 ObservationContext 上设置 error 字段:

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.filter.ServerHttpObservationFilter;

@Controller
public class UserController {

    @ExceptionHandler(MissingUserException.class)
    ResponseEntity<Void> handleMissingUser(HttpServletRequest request, MissingUserException exception) {
        // We want to record this exception with the observation
        ServerHttpObservationFilter.findObservationContext(request)
                .ifPresent(context -> context.setError(exception));
        return ResponseEntity.notFound().build();
    }

    static class MissingUserException extends RuntimeException {
    }

}

默认情况下,创建了以下 KeyValues

Table 13. Low cardinality Keys

Name

Description

exception (required)

在 exchange 过程中抛出的异常名称,如果没有发生异常,则为 KeyValue#NONE_VALUE}。

method (required)

HTTP请求方法的名称,如果不能正常接收请求,则为 "none"

outcome (required)

HTTP server exchange 的结果。

status (required)

HTTP响应的原始状态代码,如果没有创建响应,则为 "UNKNOWN"

uri (required)

匹配处理程序的URI pattern (如果有的话),对于3xx响应,回退到 REDIRECTION,对于404响应,回退到 NOT_FOUND,对于没有路径信息的请求,回退到 root,对于所有其他请求,回退到 UNKNOWN

Table 14. High cardinality Keys

Name

Description

http.url (required)

HTTP 请求 URI。

7.4.2. Reactive 应用

应用程序需要在其应用程序中配置 org.springframework.web.filter.reactive.ServerHttpObservationFilter 响应式 WebFilter。它默认使用 org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention,由 ServerRequestObservationContext 支持。

只有当 Exception 没有被Web框架处理,并冒泡到 WebFilter 时,才会将 observation 记录为错误。通常情况下,所有由Spring WebFlux的 @ExceptionHandlerProblemDetail 支持 处理的异常都不会被记录在 observation 中。你可以在请求处理的任何时候,自己在 ObservationContext 上设置 error 字段:

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
import org.springframework.web.server.ServerWebExchange;

@Controller
public class UserController {

    @ExceptionHandler(MissingUserException.class)
    ResponseEntity<Void> handleMissingUser(ServerWebExchange exchange, MissingUserException exception) {
        // We want to record this exception with the observation
        ServerHttpObservationFilter.findObservationContext(exchange)
                .ifPresent(context -> context.setError(exception));
        return ResponseEntity.notFound().build();
    }

    static class MissingUserException extends RuntimeException {
    }

}

默认情况下,创建了以下 KeyValues

Table 15. Low cardinality Keys

Name

Description

exception (required)

exchange 过程中抛出的异常的名称,如果没有发生异常,则为 "none"。

method (required)

HTTP请求方法的名称,如果不能正常接收请求,则为 "none"

outcome (required)

HTTP server exchange 的结果。

status (required)

HTTP响应的原始状态代码,如果没有创建响应,则为 "UNKNOWN"

uri (required)

URI pattern for the matching handler if available, falling back to REDIRECTION for 3xx responses, NOT_FOUND for 404 responses, root for requests with no path info, and UNKNOWN for all other requests.

匹配处理程序的 URI pattern (如果有的话),对于3xx响应,回退到 REDIRECTION,对于404响应,回退到 NOT_FOUND,对于没有路径信息的请求,回退到 root,对于所有其他请求,回退到 UNKNOWN

Table 16. High cardinality Keys

Name

Description

http.url (required)

HTTP 请求 URI。

7.5. HTTP Client 仪表

HTTP client exchange observation 是以 "http.client.requests" 的名字创建的,用于阻塞和响应式客户端。与服务器上的不同,instrumentation 直接在客户端实现,所以唯一需要的步骤是在客户端配置一个 ObservationRegistry

7.5.1. RestTemplate

应用程序必须在 RestTemplate 实例上配置 ObservationRegistry,以启用 instrumentation;没有这个,observation 是 "无用的"。Spring Boot 将自动配置 RestTemplateBuilder Bean,并已设置 observation registry。

Instrumentation 默认使用 org.springframework.http.client.observation.ClientRequestObservationConvention,由 ClientRequestObservationContext 支持。

Table 17. Low cardinality Keys

Name

Description

method (required)

HTTP请求方法的名称,如果不能创建请求,则为 "none"

uri (required)

用于HTTP请求的URI模板,如果没有提供,则为 "none"。只考虑URI的路径部分。

client.name (required)

从请求URI host 得出的客户端名称

status (required)

HTTP响应的原始状态代码,如果是 IOException 则为 "IO_ERROR",如果没有收到响应则为 "CLIENT_ERROR"

outcome (required)

HTTP client exchange 的结果。

exception (required)

exchange 过程中抛出的异常的名称,如果没有发生异常,则为 "none"

Table 18. High cardinality Keys

Name

Description

http.url (required)

HTTP 请求 URI。

7.5.2. WebClient

应用程序必须在 WebClient builder 上配置 ObservationRegistry,以启用instrumentation;没有这个,observation 是 "无用的"。Spring Boot将自动配置 WebClient.Builder Bean,并已设置 observation registry。

Instrumentation 默认使用 org.springframework.web.reactive.function.client.ClientRequestObservationConvention,由 ClientRequestObservationContext 支持。

Table 19. Low cardinality Keys

Name

Description

method (required)

HTTP请求方法的名称,如果不能创建请求,则为 "none"

uri (required)

用于HTTP请求的URI模板,如果没有提供,则为 "none"。只考虑URI的路径部分。

client.name (required)

从请求URI host 得出的客户端名称。

status (required)

HTTP响应的原始状态代码,如果是 IOException 则为 "CLIENT_ERROR",如果没有收到响应则为 "CLIENT_ERROR"

outcome (required)

HTTP client exchange 的结果。

exception (required)

exchange 过程中抛出的异常的名称,如果没有发生异常,则为 "none"

Table 20. High cardinality Keys

Name

Description

http.url (required)

HTTP 请求 URI。

8. 附录

8.1. XML Schema

附录的这一部分列出了与整合技术有关的XML schema。

8.1.1. jee Schema

jee 元素处理与Jakarta EE(企业版)配置有关的问题,如查找JNDI对象和定义EJB引用。

要使用 jee schema中的元素,你需要在Spring XML配置文件的顶部有以下序言。以下片段中的文字引用了正确的 schema,这样jee命名空间中的元素就可以为你所用:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee
        https://www.springframework.org/schema/jee/spring-jee.xsd">

    <!-- bean definitions here -->

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

下面的例子显示了如何使用 JNDI 来查询一个数据源,没有 jee schema。

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="jdbc/MyDataSource"/>
</bean>
<bean id="userDao" class="com.foo.JdbcUserDao">
    <!-- Spring will do the cast automatically (as usual) -->
    <property name="dataSource" ref="dataSource"/>
</bean>

下面的例子显示了如何使用JNDI来查询一个数据源,具有 jee schema:

<jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource"/>

<bean id="userDao" class="com.foo.JdbcUserDao">
    <!-- Spring will do the cast automatically (as usual) -->
    <property name="dataSource" ref="dataSource"/>
</bean>
<jee:jndi-lookup/> (使用单一 JNDI Environment 设置)

下面的例子显示了如何使用JNDI来查询一个没有 jee 的环境变量:

<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="jdbc/MyDataSource"/>
    <property name="jndiEnvironment">
        <props>
            <prop key="ping">pong</prop>
        </props>
    </property>
</bean>

下面的例子显示了如何使用JNDI来用 jee 查询环境变量:

<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
    <jee:environment>ping=pong</jee:environment>
</jee:jndi-lookup>
<jee:jndi-lookup/> (具有多个 JNDI Environment 设置)

下面的例子显示了如何使用JNDI来查询多个环境变量而不需要 jee

<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="jdbc/MyDataSource"/>
    <property name="jndiEnvironment">
        <props>
            <prop key="sing">song</prop>
            <prop key="ping">pong</prop>
        </props>
    </property>
</bean>

下面的例子显示了如何使用JNDI来用 jee 查询多个环境变量:

<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
    <!-- newline-separated, key-value pairs for the environment (standard Properties format) -->
    <jee:environment>
        sing=song
        ping=pong
    </jee:environment>
</jee:jndi-lookup>
<jee:jndi-lookup/> (复杂的)

下面的例子显示了如何使用JNDI来查询一个数据源和一些不同的属性,而不用 jee

<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="jdbc/MyDataSource"/>
    <property name="cache" value="true"/>
    <property name="resourceRef" value="true"/>
    <property name="lookupOnStartup" value="false"/>
    <property name="expectedType" value="com.myapp.DefaultThing"/>
    <property name="proxyInterface" value="com.myapp.Thing"/>
</bean>

下面的例子显示了如何使用JNDI来查询一个数据源和一些不同的属性,用 jee

<jee:jndi-lookup id="simple"
        jndi-name="jdbc/MyDataSource"
        cache="true"
        resource-ref="true"
        lookup-on-startup="false"
        expected-type="com.myapp.DefaultThing"
        proxy-interface="com.myapp.Thing"/>
<jee:local-slsb/> (简单的)

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

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

<bean id="simple"
        class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/RentalServiceBean"/>
    <property name="businessInterface" value="com.foo.service.RentalService"/>
</bean>

下面的例子显示了如何用 jee 配置对本地EJB无状态 Session Bean 的引用。

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

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

下面的例子显示了如何配置对本地EJB无状态 Session Bean 的引用和一些属性,而不用 jee

<bean id="complexLocalEjb"
        class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/RentalServiceBean"/>
    <property name="businessInterface" value="com.example.service.RentalService"/>
    <property name="cacheHome" value="true"/>
    <property name="lookupHomeOnStartup" value="true"/>
    <property name="resourceRef" value="true"/>
</bean>

下面的例子显示了如何配置对本地EJB无状态 Session Bean 的引用和一些属性,用 jee

<jee:local-slsb id="complexLocalEjb"
        jndi-name="ejb/RentalServiceBean"
        business-interface="com.foo.service.RentalService"
        cache-home="true"
        lookup-home-on-startup="true"
        resource-ref="true">
<jee:remote-slsb/>

<jee:remote-slsb/> 元素配置了对远程 EJB 无状态 Session Bean 的引用。

下面的例子显示了如何配置对远程EJB无状态 Session Bean 的引用,而不用 jee

<bean id="complexRemoteEjb"
        class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean">
    <property name="jndiName" value="ejb/MyRemoteBean"/>
    <property name="businessInterface" value="com.foo.service.RentalService"/>
    <property name="cacheHome" value="true"/>
    <property name="lookupHomeOnStartup" value="true"/>
    <property name="resourceRef" value="true"/>
    <property name="homeInterface" value="com.foo.service.RentalService"/>
    <property name="refreshHomeOnConnectFailure" value="true"/>
</bean>

下面的例子显示了如何用 jee 配置对远程EJB无状态 Session Bean 的引用。

<jee:remote-slsb id="complexRemoteEjb"
        jndi-name="ejb/MyRemoteBean"
        business-interface="com.foo.service.RentalService"
        cache-home="true"
        lookup-home-on-startup="true"
        resource-ref="true"
        home-interface="com.foo.service.RentalService"
        refresh-home-on-connect-failure="true">

8.1.2. jms Schema

jms 元素涉及到配置JMS相关的Bean,比如Spring的 消息监听器容器。这些元素在 JMS章节 中题为 JMS命名空间支持 的部分有详细介绍。关于这种支持和 jms 元素本身的全部细节,请参见该章。

为了完整起见,为了使用 jms schema 中的元素,你需要在Spring XML配置文件的顶部有以下序言。下面这段文字引用了正确的 schema,这样 jms 命名空间中的元素就可以为你所用:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jms="http://www.springframework.org/schema/jms"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jms
        https://www.springframework.org/schema/jms/spring-jms.xsd">

    <!-- bean definitions here -->

</beans>

8.1.3. 使用 <context:mbean-export/>

这个元素在 配置基于注解的 MBean 导出 中详细介绍。

8.1.4. cache Schema

你可以使用 cache 元素来实现对Spring的 @CacheEvict@CachePut@Caching 注解的支持。它它也支持基于XML的声明式缓存。详见 启用缓存注解基于声明式XML的缓存

为了使用 cache schema 中的元素,你需要在Spring XML配置文件的顶部有以下序言。以下片段中的文字引用了正确的 schema,这样 cache 命名空间中的元素就可以为你所用:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache
        https://www.springframework.org/schema/cache/spring-cache.xsd">

    <!-- bean definitions here -->

</beans>