Spring与多种技术的集成
本文涵盖了 Spring 对多种技术集成的最佳实践。
包括了客户端、JMS、JMX、电子邮件、任务执行、缓存抽象等内容。
# 一、客户端
Spring 框架提供了以下几种调用 REST 端点的方式:
- RestClient - 具有流畅 API 的同步客户端。
- WebClient - 具有流畅 API 的非阻塞响应式客户端。
- RestTemplate - 具有模板方法 API 的同步客户端。
- HTTP 接口 - 带有生成的动态代理实现的注解接口。
# 1、RestClient
RestClient
是一个同步的 HTTP 客户端,提供了现代的流畅 API。它对 HTTP 库进行了抽象,能够方便地将 Java 对象转换为 HTTP 请求,并从 HTTP 响应中创建对象。
# 1.1、创建 RestClient
可以使用静态的 create
方法之一来创建 RestClient
,也可以使用 builder()
来获取一个构建器,通过它可以进行更多设置,比如指定使用哪个 HTTP 库和哪些消息转换器,设置默认 URI、默认路径变量、默认请求头、uriBuilderFactory
,或者注册拦截器和初始器。
一旦创建(或构建)完成,RestClient
可以被多个线程安全地使用。
以下示例展示了如何创建默认的 RestClient
以及自定义的 RestClient
:
RestClient defaultClient = RestClient.create();
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
.baseUrl("https://example.com")
.defaultUriVariables(Map.of("variable", "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build();
# 1.2、使用 RestClient
使用 RestClient
发起 HTTP 请求时,首先要指定使用的 HTTP 方法,可以通过 method(HttpMethod)
或便捷方法 get()
、head()
、post()
等来实现。
# a、请求 URL
接下来,可以使用 uri
方法指定请求的 URI。如果 RestClient
配置了默认 URI,这一步可以省略。URL 通常以 String
形式指定,可以包含可选的 URI 模板变量。以下示例展示了如何配置一个对 https://example.com/orders/42
的 GET 请求:
int id = 42;
restClient.get()
.uri("https://example.com/orders/{id}", id)
// 其他操作
也可以使用函数进行更多控制,例如指定请求参数 (opens new window)。
默认情况下,字符串 URL 会被编码,但可以通过使用自定义的 uriBuilderFactory
构建客户端来改变这一点。URL 也可以通过函数或 java.net.URI
提供,这两种方式都不会进行编码。有关处理和编码 URI 的更多详细信息,请参阅URI 链接 (opens new window)。
# b、请求头和请求体
如有必要,可以通过 header(String, String)
、headers(Consumer<HttpHeaders>)
,或便捷方法 accept(MediaType…)
、acceptCharset(Charset…)
等来添加请求头,从而对 HTTP 请求进行操作。对于可以包含请求体的 HTTP 请求(POST
、PUT
和 PATCH
),还有额外的方法可用:contentType(MediaType)
和 contentLength(long)
。
请求体本身可以通过 body(Object)
设置,它内部使用了HTTP 消息转换。或者,可以使用 ParameterizedTypeReference
设置请求体,这样可以使用泛型。最后,还可以将请求体设置为一个回调函数,该函数会向 OutputStream
写入数据。
# c、获取响应
一旦请求设置完成,可以在 retrieve()
之后链式调用方法来发送请求。例如,可以使用 retrieve().body(Class)
或 retrieve().body(ParameterizedTypeReference)
来处理参数化类型(如列表)中的响应体。body
方法可以将响应内容转换为各种类型,例如将字节转换为 String
,使用 Jackson 将 JSON 转换为对象等等。
响应还可以转换为 ResponseEntity
,这样可以访问响应头和响应体,使用 retrieve().toEntity(Class)
即可。
注意:单独调用 retrieve()
本身不会产生任何作用,它会返回一个 ResponseSpec
。应用程序必须在 ResponseSpec
上调用终结操作才能产生副作用。如果在你的用例中不需要处理响应,可以使用 retrieve().toBodilessEntity()
。
以下示例展示了如何使用 RestClient
执行一个简单的 GET
请求:
String result = restClient.get() // (1)
.uri("https://example.com") // (2)
.retrieve() // (3)
.body(String.class); // (4)
System.out.println(result); // (5)
步骤 | 说明 |
---|---|
(1) | 设置一个 GET 请求 |
(2) | 指定要连接的 URL |
(3) | 获取响应 |
(4) | 将响应转换为字符串 |
(5) | 打印结果 |
通过 ResponseEntity
可以访问响应状态码和响应头:
ResponseEntity<String> result = restClient.get() // (1)
.uri("https://example.com") // (1)
.retrieve()
.toEntity(String.class); // (2)
System.out.println("Response status: " + result.getStatusCode()); // (3)
System.out.println("Response headers: " + result.getHeaders()); // (3)
System.out.println("Contents: " + result.getBody()); // (3)
步骤 | 说明 |
---|---|
(1) | 为指定的 URL 设置一个 GET 请求 |
(2) | 将响应转换为 ResponseEntity |
(3) | 打印结果 |
RestClient
可以使用 Jackson 库将 JSON 转换为对象。注意此示例中 URI 变量的使用,以及 Accept
头被设置为 JSON:
int id = ...;
Pet pet = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id) // (1)
.accept(APPLICATION_JSON) // (2)
.retrieve()
.body(Pet.class); // (3)
步骤 | 说明 |
---|---|
(1) | 使用 URI 变量 |
(2) | 将 Accept 头设置为 application/json |
(3) | 将 JSON 响应转换为 Pet 域对象 |
在接下来的示例中,RestClient
用于执行一个包含 JSON 的 POST 请求,同样使用 Jackson 进行转换:
Pet pet = ...; // (1)
ResponseEntity<Void> response = restClient.post() // (2)
.uri("https://petclinic.example.com/pets/new") // (2)
.contentType(APPLICATION_JSON) // (3)
.body(pet) // (4)
.retrieve()
.toBodilessEntity(); // (5)
步骤 | 说明 |
---|---|
(1) | 创建一个 Pet 域对象 |
(2) | 设置一个 POST 请求,并指定要连接的 URL |
(3) | 将 Content-Type 头设置为 application/json |
(4) | 使用 pet 作为请求体 |
(5) | 将响应转换为无实体的响应实体 |
# d、错误处理
默认情况下,当 RestClient
获取到状态码为 4xx 或 5xx 的响应时,会抛出 RestClientException
的子类。可以使用 onStatus
来覆盖此行为。
String result = restClient.get() // (1)
.uri("https://example.com/this-url-does-not-exist") // (1)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { // (2)
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); // (3)
})
.body(String.class);
步骤 | 说明 |
---|---|
(1) | 为返回 404 状态码的 URL 创建一个 GET 请求 |
(2) | 为所有 4xx 状态码设置状态处理程序 |
(3) | 抛出自定义异常 |
# e、交换
对于更高级的场景,RestClient
通过 exchange()
方法提供对底层 HTTP 请求和响应的访问,exchange()
可以替代 retrieve()
使用。使用 exchange()
时,状态处理程序将不会应用,因为交换函数已经提供了对完整响应的访问,允许你进行必要的错误处理。
Pet result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id)
.accept(APPLICATION_JSON)
.exchange((request, response) -> { // (1)
if (response.getStatusCode().is4xxClientError()) { // (2)
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); // (2)
}
else {
Pet pet = convertResponse(response); // (3)
return pet;
}
});
步骤 | 说明 |
---|---|
(1) | exchange 方法提供请求和响应 |
(2) | 当响应状态码为 4xx 时抛出异常 |
(3) | 将响应转换为 Pet 域对象 |
# 1.3、消息转换
请参阅专用部分中支持的 HTTP 消息转换器 (opens new window)。
# a、视图
要仅序列化对象属性的子集,可以指定 Jackson JSON 视图 (opens new window),如下例所示:
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);
ResponseEntity<Void> response = restClient.post() // 或者 RestTemplate.postForEntity
.contentType(APPLICATION_JSON)
.body(value)
.retrieve()
.toBodilessEntity();
# b、多部分数据
要发送多部分数据,需要提供一个 MultiValueMap<String, Object>
,其值可以是表示部分内容的 Object
、表示文件部分的 Resource
,或者是带有头信息的部分内容的 HttpEntity
。例如:
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
// 使用 RestClient.post 或 RestTemplate.postForEntity 发送
在大多数情况下,不需要为每个部分指定 Content-Type
。内容类型会根据用于序列化它的 HttpMessageConverter
自动确定,或者在 Resource
的情况下,根据文件扩展名确定。如有必要,可以使用 HttpEntity
包装器显式提供 MediaType
。
一旦 MultiValueMap
准备好,就可以使用 RestClient.post().body(parts)
(或 RestTemplate.postForObject
)将其用作 POST
请求的主体。
如果 MultiValueMap
包含至少一个非字符串值,FormHttpMessageConverter
会将 Content-Type
设置为 multipart/form-data
。如果 MultiValueMap
只有字符串值,Content-Type
默认为 application/x-www-form-urlencoded
。如有必要,也可以显式设置 Content-Type
。
# 1.4、客户端请求工厂
为了执行 HTTP 请求,RestClient
使用一个客户端 HTTP 库。这些库通过 ClientRequestFactory
接口进行适配,有多种实现可供选择:
JdkClientHttpRequestFactory
用于 Java 的HttpClient
HttpComponentsClientHttpRequestFactory
用于 Apache HTTP Components 的HttpClient
JettyClientHttpRequestFactory
用于 Jetty 的HttpClient
ReactorNettyClientRequestFactory
用于 Reactor Netty 的HttpClient
SimpleClientHttpRequestFactory
作为简单的默认实现
如果在构建 RestClient
时未指定请求工厂,若类路径上有 Apache 或 Jetty 的 HttpClient
,它将使用它们;否则,如果加载了 java.net.http
模块,它将使用 Java 的 HttpClient
;最后,它将使用简单的默认实现。
提示:请注意,SimpleClientHttpRequestFactory
在访问表示错误的响应状态(例如 401)时可能会抛出异常。如果这是一个问题,请使用其他替代的请求工厂。
# 2、WebClient
WebClient
是一个非阻塞的响应式客户端,用于执行 HTTP 请求。它于 5.0 版本引入,为 RestTemplate
提供了一种替代方案,支持同步、异步和流式处理场景。
WebClient
支持以下功能:
- 非阻塞 I/O
- 响应式流背压
- 用较少的硬件资源实现高并发
- 利用 Java 8 Lambda 的函数式风格流畅 API
- 同步和异步交互
- 与服务器之间的流式上传或下载
更多详细信息请参阅 WebClient (opens new window)。
# 3、RestTemplate
RestTemplate
以经典的 Spring 模板类的形式,为 HTTP 客户端库提供了一个高级 API。它暴露了以下几组重载方法:
注意:RestClient
为同步 HTTP 访问提供了更现代的 API。对于异步和流式处理场景,请考虑使用响应式的 WebClient (opens new window)。
方法组 | 描述 |
---|---|
getForObject | 通过 GET 请求检索表示形式。 |
getForEntity | 通过 GET 请求检索 ResponseEntity (即状态、头信息和主体)。 |
headForHeaders | 通过 HEAD 请求检索资源的所有头信息。 |
postForLocation | 通过 POST 请求创建新资源,并返回响应中的 Location 头信息。 |
postForObject | 通过 POST 请求创建新资源,并返回响应中的表示形式。 |
postForEntity | 通过 POST 请求创建新资源,并返回响应中的表示形式。 |
put | 通过 PUT 请求创建或更新资源。 |
patchForObject | 通过 PATCH 请求更新资源,并返回响应中的表示形式。请注意,JDK 的 HttpURLConnection 不支持 PATCH ,但 Apache HttpComponents 等支持。 |
delete | 通过 DELETE 请求删除指定 URI 处的资源。 |
optionsForAllow | 通过 ALLOW 请求检索资源允许的 HTTP 方法。 |
exchange | 上述方法的更通用(且更灵活)的版本,需要时提供额外的灵活性。它接受一个 RequestEntity (包括 HTTP 方法、URL、头信息和主体作为输入)并返回一个 ResponseEntity 。这些方法允许使用 ParameterizedTypeReference 而不是 Class 来指定带有泛型的响应类型。 |
execute | 执行请求的最通用方式,可以通过回调接口完全控制请求准备和响应提取。 |
# 3.1、初始化
RestTemplate
和 RestClient
使用相同的 HTTP 库抽象。默认情况下,它使用 SimpleClientHttpRequestFactory
,但可以通过构造函数进行更改。请参阅客户端请求工厂。
注意:可以对 RestTemplate
进行可观测性配置,以生成指标和跟踪信息。请参阅 RestTemplate 可观测性支持 (opens new window) 部分。
# 3.2、主体
传入和从 RestTemplate
方法返回的对象借助 HttpMessageConverter
进行 HTTP 消息的转换,请参阅HTTP 消息转换。
# 3.3、从 RestTemplate
迁移到 RestClient
下表显示了 RestTemplate
方法对应的 RestClient
等效方法,可用于从前者迁移到后者。
RestTemplate 方法 | RestClient 等效方法 |
---|---|
getForObject(String, Class, Object…) | get() .uri(String, Object…) .retrieve() .body(Class) |
getForObject(String, Class, Map) | get() .uri(String, Map) .retrieve() .body(Class) |
getForObject(URI, Class) | get() .uri(URI) .retrieve() .body(Class) |
getForEntity(String, Class, Object…) | get() .uri(String, Object…) .retrieve() .toEntity(Class) |
getForEntity(String, Class, Map) | get() .uri(String, Map) .retrieve() .toEntity(Class) |
getForEntity(URI, Class) | get() .uri(URI) .retrieve() .toEntity(Class) |
headForHeaders(String, Object…) | head() .uri(String, Object…) .retrieve() .toBodilessEntity() .getHeaders() |
headForHeaders(String, Map) | head() .uri(String, Map) .retrieve() .toBodilessEntity() .getHeaders() |
headForHeaders(URI) | head() .uri(URI) .retrieve() .toBodilessEntity() .getHeaders() |
postForLocation(String, Object, Object…) | post() .uri(String, Object…) .body(Object).retrieve() .toBodilessEntity() .getLocation() |
postForLocation(String, Object, Map) | post() .uri(String, Map) .body(Object) .retrieve() .toBodilessEntity() .getLocation() |
postForLocation(URI, Object) | post() .uri(URI) .body(Object) .retrieve() .toBodilessEntity() .getLocation() |
postForObject(String, Object, Class, Object…) | post() .uri(String, Object…) .body(Object) .retrieve() .body(Class) |
postForObject(String, Object, Class, Map) | post() .uri(String, Map) .body(Object) .retrieve() .body(Class) |
postForObject(URI, Object, Class) | post() .uri(URI) .body(Object) .retrieve() .body(Class) |
postForEntity(String, Object, Class, Object…) | post() .uri(String, Object…) .body(Object) .retrieve() .toEntity(Class) |
postForEntity(String, Object, Class, Map) | post() .uri(String, Map) .body(Object) .retrieve() .toEntity(Class) |
postForEntity(URI, Object, Class) | post() .uri(URI) .body(Object) .retrieve() .toEntity(Class) |
put(String, Object, Object…) | put() .uri(String, Object…) .body(Object) .retrieve() .toBodilessEntity() |
put(String, Object, Map) | put() .uri(String, Map) .body(Object) .retrieve() .toBodilessEntity() |
put(URI, Object) | put() .uri(URI) .body(Object) .retrieve() .toBodilessEntity() |
patchForObject(String, Object, Class, Object…) | patch() .uri(String, Object…) .body(Object) .retrieve() .body(Class) |
patchForObject(String, Object, Class, Map) | patch() .uri(String, Map) .body(Object) .retrieve() .body(Class) |
patchForObject(URI, Object, Class) | patch() .uri(URI) .body(Object) .retrieve() .body(Class) |
delete(String, Object…) | delete() .uri(String, Object…) .retrieve() .toBodilessEntity() |
delete(String, Map) | delete() .uri(String, Map) .retrieve() .toBodilessEntity() |
delete(URI) | delete() .uri(URI) .retrieve() .toBodilessEntity() |
optionsForAllow(String, Object…) | options() .uri(String, Object…) .retrieve() .toBodilessEntity() .getAllow() |
optionsForAllow(String, Map) | options() .uri(String, Map) .retrieve() .toBodilessEntity() .getAllow() |
optionsForAllow(URI) | options() .uri(URI) .retrieve() .toBodilessEntity() .getAllow() |
exchange(String, HttpMethod, HttpEntity, Class, Object…) | method(HttpMethod) .uri(String, Object…) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1] |
exchange(String, HttpMethod, HttpEntity, Class, Map) | method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1] |
exchange(URI, HttpMethod, HttpEntity, Class) | method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1] |
exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Object…) | method(HttpMethod) .uri(String, Object…) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1] |
exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Map) | method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1] |
exchange(URI, HttpMethod, HttpEntity, ParameterizedTypeReference) | method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1] |
exchange(RequestEntity, Class) | method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [2] |
exchange(RequestEntity, ParameterizedTypeReference) | method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [2] |
execute(String, HttpMethod, RequestCallback, ResponseExtractor, Object…) | method(HttpMethod) .uri(String, Object…) .exchange(ExchangeFunction) |
execute(String, HttpMethod, RequestCallback, ResponseExtractor, Map) | method(HttpMethod) .uri(String, Map) .exchange(ExchangeFunction) |
execute(URI, HttpMethod, RequestCallback, ResponseExtractor) | method(HttpMethod) .uri(URI) .exchange(ExchangeFunction) |
- [1]
HttpEntity
的头信息和主体需要通过headers(Consumer<HttpHeaders>)
和body(Object)
提供给RestClient
。 - [2]
RequestEntity
的方法、URI、头信息和主体需要通过method(HttpMethod)
、uri(URI)
、headers(Consumer<HttpHeaders>)
和body(Object)
提供给RestClient
。
# 4、接口
Spring 框架允许你将 HTTP 服务定义为带有 @HttpExchange
方法的 Java 接口。你可以将这样的接口传递给 HttpServiceProxyFactory
来创建一个代理,该代理通过诸如 RestClient
或 WebClient
之类的 HTTP 客户端执行请求。你还可以在 @Controller
中实现该接口以处理服务器请求。
首先,创建带有 @HttpExchange
方法的接口:
public interface RepositoryService {
@GetExchange("/repos/{owner}/{repo}")
Repository getRepository(@PathVariable String owner, @PathVariable String repo);
// 更多 HTTP 交换方法...
}
现在,你可以创建一个代理,在调用方法时执行请求。
对于 RestClient
:
RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
RepositoryService service = factory.createClient(RepositoryService.class);
对于 WebClient
:
WebClient webClient = WebClient.builder().baseUrl("https://api.github.com/").build();
WebClientAdapter adapter = WebClientAdapter.create(webClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
RepositoryService service = factory.createClient(RepositoryService.class);
对于 RestTemplate
:
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/"));
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
RepositoryService service = factory.createClient(RepositoryService.class);
@HttpExchange
也支持在类型级别使用,这样它将应用于所有方法:
@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
public 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);
}
# 4.1、方法参数
带有注解的HTTP交换方法支持灵活的方法签名,包含以下方法参数:
方法参数 | 描述 |
---|---|
URI | 动态设置请求的URL,覆盖注解的url 属性。 |
UriBuilderFactory | 提供一个UriBuilderFactory 来扩展URI模板和URI变量。实际上,它会替换底层客户端的UriBuilderFactory (及其基本URL)。 |
HttpMethod | 动态设置请求的HTTP方法,覆盖注解的method 属性。 |
@RequestHeader | 添加一个或多个请求头。参数可以是单个值、值的Collection<?> 、Map<String, ?> 、MultiValueMap<String, ?> 。支持非字符串值的类型转换。头值会被添加,不会覆盖已添加的头值。 |
@PathVariable | 为请求URL中的占位符添加一个变量。参数可以是包含多个变量的Map<String, ?> ,或单个值。支持非字符串值的类型转换。 |
@RequestAttribute | 提供一个Object 作为请求属性添加。仅RestClient 和WebClient 支持。 |
@RequestBody | 提供请求体,可以是要序列化的对象,或Reactive Streams的Publisher ,如Mono 、Flux ,或通过配置的ReactiveAdapterRegistry 支持的任何其他异步类型。 |
@RequestParam | 添加一个或多个请求参数。参数可以是包含多个参数的Map<String, ?> 或MultiValueMap<String, ?> 、值的Collection<?> ,或单个值。支持非字符串值的类型转换。当"content-type" 设置为"application/x-www-form-urlencoded" 时,请求参数会编码在请求体中。否则,它们会作为URL查询参数添加。 |
@RequestPart | 添加一个请求部分,可以是字符串(表单字段)、Resource (文件部分)、对象(要编码的实体,如JSON)、HttpEntity (部分内容和头)、Spring的Part ,或上述任何类型的Reactive Streams Publisher 。 |
MultipartFile | 从MultipartFile 添加一个请求部分,通常用于Spring MVC控制器中,表示上传的文件。 |
@CookieValue | 添加一个或多个cookie。参数可以是包含多个cookie的Map<String, ?> 或MultiValueMap<String, ?> 、值的Collection<?> ,或单个值。支持非字符串值的类型转换。 |
方法参数不能为null
,除非参数注解上的required
属性设置为false
,或者参数由MethodParameter#isOptional
(opens new window)标记为可选。
# 4.2、自定义参数解析器
对于更复杂的情况,HTTP接口不支持将RequestEntity
类型作为方法参数。这将接管整个HTTP请求,并且不会提升接口的语义。开发者可以将多个方法参数组合成一个自定义类型,并配置一个专门的HttpServiceArgumentResolver
实现,而不是添加大量的方法参数。
下面的HTTP接口中,我们使用一个自定义的Search
类型作为参数:
public interface RepositoryService {
@GetExchange("/repos/search")
List<Repository> searchRepository(Search search);
}
我们可以实现自己的HttpServiceArgumentResolver
,以支持自定义的Search
类型,并将其数据写入传出的HTTP请求中:
static class SearchQueryArgumentResolver implements HttpServiceArgumentResolver {
@Override
public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
if (parameter.getParameterType().equals(Search.class)) {
Search search = (Search) argument;
requestValues.addRequestParameter("owner", search.owner());
requestValues.addRequestParameter("language", search.language());
requestValues.addRequestParameter("query", search.query());
return true;
}
return false;
}
}
最后,我们可以在设置过程中使用这个参数解析器,并使用我们的HTTP接口:
RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(adapter)
.customArgumentResolver(new SearchQueryArgumentResolver())
.build();
RepositoryService repositoryService = factory.createClient(RepositoryService.class);
Search search = Search.create()
.owner("spring-projects")
.language("java")
.query("rest")
.build();
List<Repository> repositories = repositoryService.searchRepository(search);
# 4.3、返回值
支持的返回值取决于底层客户端。
# 4.4、同步返回值(如RestClient
和RestTemplate
)
适配到HttpExchangeAdapter
的客户端(如RestClient
和RestTemplate
)支持同步返回值:
方法返回值 | 描述 |
---|---|
void | 执行给定的请求。 |
HttpHeaders | 执行给定的请求并返回响应头。 |
<T> | 执行给定的请求,并将响应内容解码为声明的返回类型。 |
ResponseEntity<Void> | 执行给定的请求,并返回包含状态和头的ResponseEntity 。 |
ResponseEntity<T> | 执行给定的请求,将响应内容解码为声明的返回类型,并返回包含状态、头和已解码主体的ResponseEntity 。 |
# a、响应式返回值(如WebClient
)
适配到ReactorHttpExchangeAdapter
的客户端(如WebClient
),除了支持上述所有返回值外,还支持响应式变体。下表显示了Reactor类型,但你也可以使用通过ReactiveAdapterRegistry
支持的其他响应式类型:
方法返回值 | 描述 |
---|---|
Mono<Void> | 执行给定的请求,并释放响应内容(如果有)。 |
Mono<HttpHeaders> | 执行给定的请求,释放响应内容(如果有),并返回响应头。 |
Mono<T> | 执行给定的请求,并将响应内容解码为声明的返回类型。 |
Flux<T> | 执行给定的请求,并将响应内容解码为声明元素类型的流。 |
Mono<ResponseEntity<Void>> | 执行给定的请求,释放响应内容(如果有),并返回包含状态和头的ResponseEntity 。 |
Mono<ResponseEntity<T>> | 执行给定的请求,将响应内容解码为声明的返回类型,并返回包含状态、头和已解码主体的ResponseEntity 。 |
Mono<ResponseEntity<Flux<T>>> | 执行给定的请求,将响应内容解码为声明元素类型的流,并返回包含状态、头和已解码响应主体流的ResponseEntity 。 |
默认情况下,使用ReactorHttpExchangeAdapter
的同步返回值的超时时间取决于底层HTTP客户端的配置。你也可以在适配器级别设置blockTimeout
值,但我们建议依赖底层HTTP客户端的超时设置,因为它在更低的级别操作,提供更多的控制。
# 4.5、错误处理
要自定义错误响应处理,你需要配置底层的HTTP客户端。
# a、使用RestClient
默认情况下,RestClient
会为4xx和5xx HTTP状态码抛出RestClientException
。要自定义此行为,请注册一个适用于通过客户端执行的所有响应的响应状态处理程序:
RestClient restClient = RestClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...)
.build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
有关更多详细信息和选项(如抑制错误状态码),请参阅RestClient.Builder
中defaultStatusHandler
的Javadoc。
# b、使用WebClient
默认情况下,WebClient
会为4xx和5xx HTTP状态码抛出WebClientResponseException
。要自定义此行为,请注册一个适用于通过客户端执行的所有响应的响应状态处理程序:
WebClient webClient = WebClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
.build();
WebClientAdapter adapter = WebClientAdapter.create(webClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(adapter).build();
有关更多详细信息和选项(如抑制错误状态码),请参阅WebClient.Builder
中defaultStatusHandler
的Javadoc。
# c、使用RestTemplate
默认情况下,RestTemplate
会为4xx和5xx HTTP状态码抛出RestClientException
。要自定义此行为,请注册一个适用于通过客户端执行的所有响应的错误处理程序:
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(myErrorHandler);
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
# 二、JMS(Java 消息服务)
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 会话和消息生产者暴露给用户。
org.springframework.jms.support
包提供了 JMSException
转换功能。该转换将受检查的 JMSException
层次结构转换为对应的不受检查的异常层次结构。如果存在受检查的 jakarta.jms.JMSException
的特定于提供者的子类,则此异常将被包装在不受检查的 UncategorizedJmsException
中。
org.springframework.jms.support.converter
包提供了一个 MessageConverter
抽象,用于在 Java 对象和 JMS 消息之间进行转换。
org.springframework.jms.support.destination
包提供了各种管理 JMS 目标的策略,例如为存储在 JNDI 中的目标提供服务定位器。
org.springframework.jms.annotation
包提供了使用 @JmsListener
来支持注解驱动的监听器端点所需的基础结构。
org.springframework.jms.config
包提供了 jms
命名空间的解析器实现,以及用于配置监听器容器和创建监听器端点的 Java 配置支持。
最后,org.springframework.jms.connection
包提供了一个适用于独立应用程序的 ConnectionFactory
实现。它还包含一个用于 JMS 的 Spring PlatformTransactionManager
实现(巧妙地命名为 JmsTransactionManager
)。这允许将 JMS 作为事务资源无缝集成到 Spring 的事务管理机制中。
# 1、注意
从 Spring 框架 5 开始,Spring 的 JMS 包完全支持 JMS 2.0,并且要求在运行时存在 JMS 2.0 API。我们建议使用与 JMS 2.0 兼容的提供者。
如果你的系统中恰好使用了较旧的消息代理,你可以尝试将现有代理升级到与 JMS 2.0 兼容的驱动程序。或者,你也可以尝试使用基于 JMS 1.1 的驱动程序运行,只需将 JMS 2.0 API 的 JAR 放在类路径中,但仅针对驱动程序使用与 JMS 1.1 兼容的 API。Spring 的 JMS 支持默认遵循 JMS 1.1 规范,因此通过相应的配置,它确实支持这种情况。不过,请仅在过渡场景中考虑这种方式。
# 2、使用 Spring JMS
本节介绍如何使用 Spring 的 JMS 组件。
# 2.1、使用 JmsTemplate
JmsTemplate
类是 JMS 核心包中的核心类。它简化了 JMS 的使用,因为在发送或同步接收消息时,它会处理资源的创建和释放。
使用 JmsTemplate
的代码只需实现回调接口,这些接口为其提供了清晰定义的高级契约。MessageCreator
回调接口在获得 JmsTemplate
中调用代码提供的 Session
时创建消息。为了更复杂地使用 JMS API,SessionCallback
提供 JMS 会话,ProducerCallback
则公开一对 Session
和 MessageProducer
。
JMS API 公开了两种发送方法,一种接受传递模式、优先级和存活时间作为服务质量(QoS)参数,另一种不接受 QoS 参数,使用默认值。由于 JmsTemplate
有许多发送方法,QoS 参数被作为 Bean 属性公开,以避免发送方法数量上的重复。同样,同步接收调用的超时值通过 setReceiveTimeout
属性设置。
一些 JMS 提供商允许通过配置 ConnectionFactory
以管理方式设置默认 QoS 值。这会导致调用 MessageProducer
实例的 send
方法(send(Destination destination, Message message)
)时使用的 QoS 默认值与 JMS 规范中指定的不同。为了提供一致的 QoS 值管理,必须通过将布尔属性 isExplicitQosEnabled
设置为 true
来明确启用 JmsTemplate
使用自己的 QoS 值。
为了方便起见,JmsTemplate
还公开了一个基本的请求 - 响应操作,允许发送消息并在作为操作一部分创建的临时队列上等待响应。
重要提示:一旦配置完成,JmsTemplate
类的实例是线程安全的。这很重要,因为这意味着你可以配置一个 JmsTemplate
实例,然后将这个共享引用安全地注入到多个协作者中。需要明确的是,JmsTemplate
是有状态的,因为它维护了对 ConnectionFactory
的引用,但这个状态不是会话状态。
从 Spring Framework 4.1 开始,JmsMessagingTemplate
基于 JmsTemplate
构建,并提供了与消息抽象(即 org.springframework.messaging.Message
)的集成。这使你能够以通用的方式创建要发送的消息。
# 2.2、连接
JmsTemplate
需要引用 ConnectionFactory
。ConnectionFactory
是 JMS 规范的一部分,是使用 JMS 的入口点。客户端应用程序将其用作工厂来与 JMS 提供商创建连接,并封装各种配置参数,其中许多是特定于供应商的,比如 SSL 配置选项。
在 EJB 中使用 JMS 时,供应商提供 JMS 接口的实现,以便它们可以参与声明式事务管理并执行连接和会话的池化操作。为了使用此实现,Jakarta EE 容器通常要求你在 EJB 或 Servlet 部署描述符中将 JMS 连接工厂声明为 resource-ref
。为了确保在 EJB 中使用 JmsTemplate
时能使用这些功能,客户端应用程序应确保引用 ConnectionFactory
的托管实现。
# a、缓存消息资源
标准 API 涉及创建许多中间对象。要发送消息,需要执行以下“API 步骤”:
ConnectionFactory -> Connection -> Session -> MessageProducer -> send
在 ConnectionFactory
和 Send
操作之间,会创建和销毁三个中间对象。为了优化资源使用并提高性能,Spring 提供了 ConnectionFactory
的两种实现。
# b、使用 SingleConnectionFactory
Spring 提供了 ConnectionFactory
接口的实现 SingleConnectionFactory
,它在所有 createConnection()
调用中返回同一个 Connection
,并忽略 close()
调用。这在测试和独立环境中很有用,这样同一个连接可以用于多个 JmsTemplate
调用,这些调用可能跨越任意数量的事务。SingleConnectionFactory
引用一个标准的 ConnectionFactory
,该工厂通常来自 JNDI。
# c、使用 CachingConnectionFactory
CachingConnectionFactory
扩展了 SingleConnectionFactory
的功能,并增加了对 Session
、MessageProducer
和 MessageConsumer
实例的缓存。初始缓存大小设置为 1
。你可以使用 sessionCacheSize
属性来增加缓存会话的数量。请注意,实际缓存的会话数量会多于该数量,因为会话是根据其确认模式进行缓存的,所以当 sessionCacheSize
设置为 1 时,最多可以有四个缓存会话实例(每种确认模式一个)。MessageProducer
和 MessageConsumer
实例在其所属的会话中缓存,并且在缓存时也会考虑生产者和消费者的唯一属性。MessageProducer
根据其目标进行缓存,MessageConsumer
根据由目标、选择器、非本地传递标志和持久订阅名称(如果创建持久消费者)组成的键进行缓存。
注意:临时队列和主题(TemporaryQueue/TemporaryTopic)的 MessageProducer
和 MessageConsumer
永远不会被缓存。不幸的是,WebLogic JMS 在其常规目标实现中实现了临时队列/主题接口,错误地表明其所有目标都不能被缓存。请在 WebLogic 上使用不同的连接池/缓存,或者为 WebLogic 定制 CachingConnectionFactory
。
# 2.3、目标管理
与 ConnectionFactory
实例一样,目标是 JMS 管理对象,你可以在 JNDI 中存储和检索它们。在配置 Spring 应用程序上下文时,你可以使用 JNDI JndiObjectFactoryBean
工厂类或 <jee:jndi-lookup>
对对象的 JMS 目标引用执行依赖注入。但是,如果应用程序中有大量目标,或者 JMS 提供商有独特的高级目标管理功能,这种策略往往很繁琐。此类高级目标管理的示例包括创建动态目标或支持目标的分层命名空间。JmsTemplate
将目标名称的解析委托给实现 DestinationResolver
接口的 JMS 目标对象。DynamicDestinationResolver
是 JmsTemplate
使用的默认实现,适用于解析动态目标。还提供了 JndiDestinationResolver
,它可作为 JNDI 中目标的服务定位器,并在必要时回退到 DynamicDestinationResolver
的行为。
通常,JMS 应用程序中使用的目标只有在运行时才知道,因此在应用程序部署时无法在管理层面创建。这通常是因为交互系统组件之间存在共享应用程序逻辑,这些组件根据著名的命名约定在运行时创建目标。尽管创建动态目标不是 JMS 规范的一部分,但大多数供应商都提供了此功能。动态目标使用用户定义的名称创建,这使它们与临时目标区分开来,并且它们通常不在 JNDI 中注册。用于创建动态目标的 API 因供应商而异,因为与目标关联的属性是特定于供应商的。然而,供应商有时会做出的一个简单实现选择是忽略 JMS 规范中的警告,并使用 TopicSession
createTopic(String topicName)
方法或 QueueSession
createQueue(String queueName)
方法来使用默认目标属性创建新目标。根据供应商的实现,DynamicDestinationResolver
随后也可以创建物理目标,而不仅仅是解析目标。
布尔属性 pubSubDomain
用于配置 JmsTemplate
知晓正在使用的 JMS 域。默认情况下,此属性的值为 false,表示将使用点对点域 Queues
。此属性(JmsTemplate
使用)通过 DestinationResolver
接口的实现确定动态目标解析的行为。
你还可以通过属性 defaultDestination
为 JmsTemplate
配置默认目标。默认目标用于不引用特定目标的发送和接收操作。
# 2.4、消息监听器容器
在 EJB 领域,JMS 消息最常见的用途之一是驱动消息驱动 Bean(MDB)。Spring 提供了一种创建消息驱动 POJO(MDP)的解决方案,这种方式不会让用户依赖于 EJB 容器。(有关 Spring 的 MDP 支持的详细介绍,请参阅异步接收:消息驱动 POJO)。端点方法可以使用 @JmsListener
进行注解,更多详细信息请参阅注解驱动的监听器端点。
消息监听器容器用于从 JMS 消息队列接收消息,并驱动注入其中的 MessageListener
。监听器容器负责消息接收的所有线程处理,并将消息分发给监听器进行处理。消息监听器容器是 MDP 和消息传递提供商之间的中介,负责注册接收消息、参与事务、获取和释放资源、异常转换等。这使你能够编写与接收消息(并可能做出响应)相关的(可能复杂的)业务逻辑,并将样板式的 JMS 基础设施问题委托给框架处理。
Spring 封装了两个标准的 JMS 消息监听器容器,每个容器都有其特殊的功能集。
SimpleMessageListenerContainer
DefaultMessageListenerContainer
# a、使用 SimpleMessageListenerContainer
此消息监听器容器是两种标准类型中较简单的一种。它在启动时创建固定数量的 JMS 会话和消费者,使用标准的 JMS MessageConsumer.setMessageListener()
方法注册监听器,并让 JMS 提供商执行监听器回调。这种变体不允许动态适应运行时需求,也不参与外部管理的事务。在兼容性方面,它与独立的 JMS 规范非常接近,但通常与 Jakarta EE 的 JMS 限制不兼容。
注意:虽然 SimpleMessageListenerContainer
不允许参与外部管理的事务,但它支持原生 JMS 事务。要启用此功能,你可以将 sessionTransacted
标志设置为 true
,或者在 XML 命名空间中将 acknowledge
属性设置为 transacted
。然后,从监听器抛出的异常会导致回滚,消息将被重新传递。或者,考虑使用 CLIENT_ACKNOWLEDGE
模式,在出现异常时它也提供重新传递功能,但不使用已事务化的 Session
实例,因此不会将任何其他 Session
操作(如发送响应消息)包含在事务协议中。
重要提示:默认的 AUTO_ACKNOWLEDGE
模式不能提供适当的可靠性保证。当监听器执行失败时(因为提供商在调用监听器后自动确认每个消息,且没有异常传播给提供商),或者当监听器容器关闭时(你可以通过设置 acceptMessagesWhileStopping
标志来配置此行为),消息可能会丢失。如果需要可靠性(例如,对于可靠的队列处理和持久主题订阅),请确保使用已事务化的会话。
# b、使用 DefaultMessageListenerContainer
此消息监听器容器在大多数情况下使用。与 SimpleMessageListenerContainer
不同,此容器变体允许动态适应运行时需求,并且能够参与外部管理的事务。当配置了 JtaTransactionManager
时,每个接收到的消息都会注册到一个 XA 事务中。因此,处理可以利用 XA 事务语义。此监听器容器在对 JMS 提供商的低要求、高级功能(如参与外部管理的事务)和与 Jakarta EE 环境的兼容性之间取得了很好的平衡。
你可以自定义容器的缓存级别。请注意,当未启用缓存时,每次接收消息都会创建一个新连接和一个新会话。高负载下将此情况与非持久订阅结合使用可能会导致消息丢失。在这种情况下,请确保使用适当的缓存级别。
当代理关闭时,此容器还具有可恢复功能。默认情况下,一个简单的 BackOff
实现每五秒进行一次重试。你可以指定自定义的 BackOff
实现以获得更细粒度的恢复选项。有关示例,请参阅 ExponentialBackOff (opens new window)。
注意:与它的兄弟 SimpleMessageListenerContainer
一样,DefaultMessageListenerContainer
支持原生 JMS 事务并允许自定义确认模式。如果你的场景可行,强烈建议使用原生 JMS 事务而不是外部管理的事务,也就是说,如果你能接受在 JVM 崩溃时偶尔出现的重复消息。你可以在业务逻辑中采取自定义的重复消息检测步骤来处理这种情况,例如,通过检查业务实体是否存在或检查协议表。任何此类安排都比另一种选择高效得多:将整个处理过程包装在一个 XA 事务中(通过使用 JtaTransactionManager
配置 DefaultMessageListenerContainer
),以涵盖 JMS 消息的接收以及消息监听器中业务逻辑的执行(包括数据库操作等)。
重要提示:默认的 AUTO_ACKNOWLEDGE
模式不能提供适当的可靠性保证。当监听器执行失败时(因为提供商在调用监听器后自动确认每个消息,且没有异常传播给提供商),或者当监听器容器关闭时(你可以通过设置 acceptMessagesWhileStopping
标志来配置此行为),消息可能会丢失。如果需要可靠性(例如,对于可靠的队列处理和持久主题订阅),请确保使用已事务化的会话。
# 2.5、事务管理
Spring 提供了 JmsTransactionManager
,用于管理单个 JMS ConnectionFactory
的事务。这使 JMS 应用程序能够利用 Spring 的管理事务功能,如数据访问章节的事务管理部分中所述。JmsTransactionManager
执行本地资源事务,将指定 ConnectionFactory
中的 JMS 连接/会话对绑定到线程。JmsTemplate
会自动检测此类事务资源并相应地进行操作。
在 Jakarta EE 环境中,ConnectionFactory
会对连接和会话实例进行池化,因此这些资源可以在事务之间得到有效重用。在独立环境中,使用 Spring 的 SingleConnectionFactory
会导致共享一个 JMS Connection
,而每个事务都有自己独立的 Session
。或者,可以考虑使用特定于提供商的池化适配器,例如 ActiveMQ 的 PooledConnectionFactory
类。
你还可以将 JmsTemplate
与 JtaTransactionManager
和支持 XA 的 JMS ConnectionFactory
一起使用,以执行分布式事务。请注意,这需要使用 JTA 事务管理器以及进行了正确 XA 配置的 ConnectionFactory
(请查阅你的 Jakarta EE 服务器或 JMS 提供商的文档)。
当使用 JMS API 从 Connection
创建 Session
时,在受管理和不受管理的事务环境中重用代码可能会令人困惑。这是因为 JMS API 只有一个工厂方法来创建 Session
,并且它需要事务和确认模式的值。在受管理的环境中,设置这些值是环境的事务基础设施的责任,因此供应商对 JMS 连接的包装器会忽略这些值。当你在不受管理的环境中使用 JmsTemplate
时,你可以通过使用属性 sessionTransacted
和 sessionAcknowledgeMode
来指定这些值。当你将 PlatformTransactionManager
与 JmsTemplate
一起使用时,模板始终会获得一个事务性的 JMS Session
。
# 3、发送消息
JmsTemplate
包含许多方便的方法来发送消息。一些发送方法通过 jakarta.jms.Destination
对象指定目标,而另一些方法则通过 JNDI 查找中的 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)
方法允许你通过目标的字符串名称发送消息。如果这些名称在 JNDI 中注册过,你应该将模板的 destinationResolver
属性设置为 JndiDestinationResolver
的实例。
如果你创建了 JmsTemplate
并指定了默认目标,send(MessageCreator c)
会将消息发送到该目标。
# 4、使用消息转换器
为了便于发送领域模型对象,JmsTemplate
有各种发送方法,这些方法将 Java 对象作为消息数据内容的参数。JmsTemplate
中的重载方法 convertAndSend()
和 receiveAndConvert()
将转换过程委托给 MessageConverter
接口的实例。该接口定义了一个简单的契约,用于在 Java 对象和 JMS 消息之间进行转换。默认实现(SimpleMessageConverter
)支持在 String
和 TextMessage
、byte[]
和 BytesMessage
以及 java.util.Map
和 MapMessage
之间进行转换。通过使用转换器,你和你的应用程序代码可以专注于通过 JMS 发送或接收的业务对象,而不必关心它如何表示为 JMS 消息的细节。
沙箱中目前包含一个 MapMessageConverter
,它使用反射在 JavaBean 和 MapMessage
之间进行转换。你可能自己实现的其他流行选择是使用现有的 XML 编组包(如 JAXB 或 XStream)来创建表示对象的 TextMessage
的转换器。
为了适应设置消息的属性、头信息和正文(这些可能无法通用地封装在转换器类中),MessagePostProcessor
接口允许你在消息转换后但在发送前访问消息。以下示例展示了如何在将 java.util.Map
转换为消息后修改消息头和属性:
public void sendWithConversion() {
Map map = new HashMap();
map.put("Name", "Mark");
map.put("Age", new Integer(47));
jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() {
public Message postProcessMessage(Message message) throws JMSException {
message.setIntProperty("AccountID", 1234);
message.setJMSCorrelationID("123-00001");
return message;
}
});
}
这将生成以下形式的消息:
MapMessage={
Header={
... 标准头信息 ...
CorrelationID={123-00001}
}
Properties={
AccountID={Integer:1234}
}
Fields={
Name={String:Mark}
Age={Integer:47}
}
}
# 5、使用 SessionCallback
和 ProducerCallback
虽然发送操作涵盖了许多常见的使用场景,但有时你可能希望在 JMS Session
或 MessageProducer
上执行多个操作。SessionCallback
和 ProducerCallback
分别暴露了 JMS Session
和 Session
/ MessageProducer
对。JmsTemplate
上的 execute()
方法运行这些回调方法。
# 6、接收消息
本文介绍了如何在 Spring 中使用 JMS 接收消息。
# 6.1、同步接收
尽管 JMS 通常与异步处理相关联,但你也可以同步消费消息。重载的 receive(..)
方法提供了此功能。在同步接收期间,调用线程会阻塞,直到有消息可用。这可能是一个危险的操作,因为调用线程有可能会无限期阻塞。receiveTimeout
属性指定了接收器在放弃等待消息之前应该等待的时长。
# 6.2、异步接收:消息驱动的 POJO
注意:Spring 还支持通过使用 @JmsListener
注解的带注解的监听器端点,并提供了开放的基础设施来以编程方式注册端点。这是目前设置异步接收器最方便的方式。更多详细信息,请参阅启用监听器端点注解。
与 EJB 世界中的消息驱动 Bean(MDB)类似,消息驱动的 POJO(MDP)充当 JMS 消息的接收器。MDP 的一个限制条件(但请参阅使用 MessageListenerAdapter
)是它必须实现 jakarta.jms.MessageListener
接口。请注意,如果你的 POJO 在多个线程上接收消息,确保你的实现是线程安全的非常重要。
以下示例展示了一个简单的 MDP 实现:
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
):
@Bean
ExampleListener messageListener() {
return new ExampleListener();
}
@Bean
DefaultMessageListenerContainer jmsContainer(ConnectionFactory connectionFactory, Destination destination,
ExampleListener messageListener) {
DefaultMessageListenerContainer jmsContainer = new DefaultMessageListenerContainer();
jmsContainer.setConnectionFactory(connectionFactory);
jmsContainer.setDestination(destination);
jmsContainer.setMessageListener(messageListener);
return jmsContainer;
}
<!-- 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>
有关每个实现所支持的功能的完整描述,请参阅各种消息监听器容器的 Spring Java 文档(所有这些容器都实现了 MessageListenerContainer (opens new window))。
# 6.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 自带的所有消息监听器容器实现都支持实现 MessageListener
或 SessionAwareMessageListener
接口的 MDP。实现 SessionAwareMessageListener
的类需要注意的是,它们会通过该接口与 Spring 绑定。是否使用它的选择完全取决于你作为应用程序开发者或架构师的决定。
需要注意的是,SessionAwareMessageListener
接口的 onMessage(..)
方法会抛出 JMSException
。与标准的 JMS MessageListener
接口不同,使用 SessionAwareMessageListener
接口时,处理任何抛出的异常是客户端代码的责任。
# 6.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 {
@Override
public void handleMessage(String message) {
// ...
}
@Override
public void handleMessage(Map message) {
// ...
}
@Override
public void handleMessage(byte[] message) {
// ...
}
@Override
public void handleMessage(Serializable message) {
// ...
}
}
特别要注意,MessageDelegate
接口的上述实现(DefaultMessageDelegate
类)根本没有任何 JMS 依赖项。它确实是一个可以通过以下配置变成 MDP 的 POJO:
@Bean
MessageListenerAdapter messageListener(DefaultMessageDelegate messageDelegate) {
return new MessageListenerAdapter(messageDelegate);
}
@Bean
DefaultMessageListenerContainer jmsContainer(ConnectionFactory connectionFactory, Destination destination,
ExampleListener messageListener) {
DefaultMessageListenerContainer jmsContainer = new DefaultMessageListenerContainer();
jmsContainer.setConnectionFactory(connectionFactory);
jmsContainer.setDestination(destination);
jmsContainer.setMessageListener(messageListener);
return jmsContainer;
}
<!-- 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>
下一个示例展示了另一个只能处理接收 JMS TextMessage
消息的 MDP。注意消息处理方法实际上被命名为 receive
(MessageListenerAdapter
中消息处理方法的默认名称是 handleMessage
),但它是可配置的(如本节后面所见)。还要注意 receive(..)
方法是如何进行强类型定义,仅接收和响应 JMS TextMessage
消息的。以下代码展示了 TextMessageDelegate
接口的定义:
public interface TextMessageDelegate {
void receive(TextMessage message);
}
以下代码展示了一个实现 TextMessageDelegate
接口的类:
public class DefaultTextMessageDelegate implements TextMessageDelegate {
@Override
public void receive(TextMessage message) {
// ...
}
}
相应的 MessageListenerAdapter
的配置如下:
@Bean
MessageListenerAdapter messageListener(DefaultTextMessageDelegate messageDelegate) {
MessageListenerAdapter messageListener = new MessageListenerAdapter(messageDelegate);
messageListener.setDefaultListenerMethod("receive");
// 我们不希望自动提取消息上下文
messageListener.setMessageConverter(null);
return messageListener;
}
<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="jmsexample.DefaultTextMessageDelegate"/>
</constructor-arg>
<property name="defaultListenerMethod" value="receive"/>
<!-- 我们不希望自动提取消息上下文 -->
<property name="messageConverter">
<null/>
</property>
</bean>
请注意,如果 messageListener
接收到的 JMS Message
类型不是 TextMessage
,则会抛出 IllegalStateException
(随后会被捕获)。MessageListenerAdapter
类的另一个功能是,如果处理方法返回非空值,它能够自动发送回一个响应 Message
。考虑以下接口和类:
public interface ResponsiveTextMessageDelegate {
// 注意返回类型...
String receive(TextMessage message);
}
public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate {
@Override
public String receive(TextMessage message) {
return "message";
}
}
如果你将 DefaultResponsiveTextMessageDelegate
与 MessageListenerAdapter
结合使用,'receive(..)'
方法执行返回的任何非空值(在默认配置下)都会被转换为 TextMessage
。生成的 TextMessage
会被发送到原始 Message
的 JMS Reply-To
属性中定义的 Destination
(如果存在),或者发送到 MessageListenerAdapter
上设置的默认 Destination
(如果已配置)。如果没有找到 Destination
,则会抛出 InvalidDestinationException
(请注意,此异常不会被捕获,而是会传播到调用栈上)。
# 6.5、事务中处理消息
在事务中调用消息监听器只需要重新配置监听器容器。
你可以通过监听器容器定义中的 sessionTransacted
标志来激活本地资源事务。然后,每次消息监听器调用都在一个活动的 JMS 事务中进行,如果监听器执行失败,消息接收将回滚。发送响应消息(通过 SessionAwareMessageListener
)是同一个本地事务的一部分,但任何其他资源操作(如数据库访问)则独立进行。这通常需要在监听器实现中进行重复消息检测,以处理数据库处理已提交但消息处理提交失败的情况。
考虑以下 bean 定义:
@Bean
DefaultMessageListenerContainer jmsContainer(ConnectionFactory connectionFactory, Destination destination,
ExampleListener messageListener) {
DefaultMessageListenerContainer jmsContainer = new DefaultMessageListenerContainer();
jmsContainer.setConnectionFactory(connectionFactory);
jmsContainer.setDestination(destination);
jmsContainer.setMessageListener(messageListener);
jmsContainer.setSessionTransacted(true);
return jmsContainer;
}
<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
JtaTransactionManager transactionManager() {
return new JtaTransactionManager();
}
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
然后,我们需要将其添加到之前的容器配置中。容器会处理其余的事情。以下示例展示了如何实现:
@Bean
DefaultMessageListenerContainer jmsContainer(ConnectionFactory connectionFactory, Destination destination,
ExampleListener messageListener) {
DefaultMessageListenerContainer jmsContainer = new DefaultMessageListenerContainer();
jmsContainer.setConnectionFactory(connectionFactory);
jmsContainer.setDestination(destination);
jmsContainer.setMessageListener(messageListener);
jmsContainer.setSessionTransacted(true);
return jmsContainer;
}
<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"/>
</bean>
# 7、对JCA消息端点的支持
从2.5版本开始,Spring还提供了对基于JCA的MessageListener
容器的支持。JmsMessageEndpointManager
会尝试从提供者的ResourceAdapter
类名中自动确定ActivationSpec
类名。因此,通常可以提供Spring的通用JmsActivationSpecConfig
,如下示例所示:
# 7.1、Java代码示例
@Bean
public JmsMessageEndpointManager jmsMessageEndpointManager(ResourceAdapter resourceAdapter,
MessageListener myMessageListener) {
JmsActivationSpecConfig specConfig = new JmsActivationSpecConfig();
specConfig.setDestinationName("myQueue");
JmsMessageEndpointManager endpointManager = new JmsMessageEndpointManager();
endpointManager.setResourceAdapter(resourceAdapter);
endpointManager.setActivationSpecConfig(specConfig);
endpointManager.setMessageListener(myMessageListener);
return endpointManager;
}
# 7.2、XML配置示例
<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
对象来设置JmsMessageEndpointManager
。ActivationSpec
对象也可以来自JNDI查找(使用<jee:jndi-lookup>
)。以下示例展示了如何操作:
# 7.3、Java代码示例
@Bean
JmsMessageEndpointManager jmsMessageEndpointManager(ResourceAdapter resourceAdapter,
MessageListener myMessageListener) {
ActiveMQActivationSpec spec = new ActiveMQActivationSpec();
spec.setDestination("myQueue");
spec.setDestinationType("jakarta.jms.Queue");
JmsMessageEndpointManager endpointManager = new JmsMessageEndpointManager();
endpointManager.setResourceAdapter(resourceAdapter);
endpointManager.setActivationSpec(spec);
endpointManager.setMessageListener(myMessageListener);
return endpointManager;
}
# 7.4、XML配置示例
<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>
有关更多详细信息,请参阅JmsMessageEndpointManager
(opens new window)、JmsActivationSpecConfig
(opens new window)和ResourceAdapterFactoryBean
(opens new window)的Java文档。
Spring还提供了一个不与JMS绑定的通用JCA消息端点管理器:org.springframework.jca.endpoint.GenericMessageEndpointManager
。该组件允许使用任何消息监听器类型(如JMS的MessageListener
)和任何特定于提供者的ActivationSpec
对象。请参阅你的JCA提供者的文档,了解你的连接器的实际功能,并参阅GenericMessageEndpointManager
(opens new window)的Java文档以获取特定于Spring的配置详细信息。
注意:基于JCA的消息端点管理与EJB 2.1消息驱动Bean非常相似,它们使用相同的底层资源提供者契约。与EJB 2.1 MDB一样,你也可以在Spring上下文中使用JCA提供者支持的任何消息监听器接口。不过,Spring为JMS提供了明确的“便利”支持,因为JMS是JCA端点管理契约中最常用的端点API。
# 8、注解驱动的监听器端点
异步接收消息最简单的方法是使用带注解的监听器端点基础设施。简而言之,它允许你将托管 bean 的一个方法公开为 JMS 监听器端点。以下示例展示了如何使用它:
@Component
public class MyService {
@JmsListener(destination = "myDestination")
public void processOrder(String data) { ... }
}
上述示例的思路是,只要 jakarta.jms.Destination
myDestination
上有可用消息,processOrder
方法就会相应地被调用(在这种情况下,会传入 JMS 消息的内容,类似于 MessageListenerAdapter
所提供的功能)。
带注解的端点基础设施会在幕后使用 JmsListenerContainerFactory
为每个带注解的方法创建一个消息监听器容器。这样的容器不会在应用上下文中注册,但可以使用 JmsListenerEndpointRegistry
bean 轻松定位以进行管理。
提示:@JmsListener
在 Java 8 中是可重复注解,因此你可以通过添加额外的 @JmsListener
声明,将多个 JMS 目的地关联到同一个方法。
# 8.1、启用监听器端点注解
要启用对 @JmsListener
注解的支持,你可以在你的某个 @Configuration
类中添加 @EnableJms
,如下例所示:
@Configuration
@EnableJms
public class JmsConfiguration {
@Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory,
DestinationResolver destinationResolver) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setDestinationResolver(destinationResolver);
factory.setSessionTransacted(true);
factory.setConcurrency("3-10");
return factory;
}
}
<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>
默认情况下,该基础设施会查找名为 jmsListenerContainerFactory
的 bean,将其作为创建消息监听器容器的工厂。在这种情况下(忽略 JMS 基础设施的设置),你可以使用一个核心线程池大小为 3、最大线程池大小为 10 的线程池来调用 processOrder
方法。
你可以为每个注解自定义要使用的监听器容器工厂,或者通过实现 JmsListenerConfigurer
接口来配置一个显式的默认工厂。只有当至少有一个端点在注册时没有指定特定的容器工厂时,才需要默认工厂。有关详细信息和示例,请参阅实现 JmsListenerConfigurer (opens new window) 的类的 JavaDoc。
# 8.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 -> {
// 处理逻辑
});
registrar.registerEndpoint(endpoint);
}
}
在上述示例中,我们使用了 SimpleJmsListenerEndpoint
,它提供了要调用的实际 MessageListener
。不过,你也可以构建自己的端点变体来描述自定义的调用机制。
请注意,你可以完全不使用 @JmsListener
,而是通过 JmsListenerConfigurer
以编程方式仅注册你的端点。
# 8.3、带注解的端点方法签名
到目前为止,我们在端点中注入的是一个简单的 String
,但实际上它的方法签名可以非常灵活。在以下示例中,我们将其重写为注入带有自定义头的 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(例如,用于发送自定义回复)。- 代表传入 JMS 消息的
org.springframework.messaging.Message
。请注意,此消息同时包含自定义头和标准头(由JmsHeaders
定义)。 - 带有
@Header
注解的方法参数,用于提取特定的头值,包括标准 JMS 头。 - 带有
@Headers
注解的参数,该参数还必须可以赋值给java.util.Map
,以便访问所有头。 - 未加注解且不是受支持类型(
Message
或Session
)的元素将被视为有效负载。你可以通过使用@Payload
注解该参数来明确表示。你还可以通过添加额外的@Valid
来开启验证。
注入 Spring 的 Message
抽象的能力特别有用,这样可以从特定于传输的消息中存储的所有信息中受益,而无需依赖特定于传输的 API。以下示例展示了如何操作:
@JmsListener(destination = "myDestination")
public void processOrder(Message<Order> order) { ... }
方法参数的处理由 DefaultMessageHandlerMethodFactory
提供,你可以进一步自定义它以支持其他方法参数。你也可以在那里自定义转换和验证支持。
例如,如果我们想在处理 Order
之前确保其有效,我们可以使用 @Valid
注解有效负载并配置必要的验证器,如下例所示:
@Configuration
@EnableJms
public class AppConfig implements JmsListenerConfigurer {
@Override
public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(myJmsHandlerMethodFactory());
}
@Bean
public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setValidator(myValidator());
return factory;
}
}
# 8.4、响应管理
MessageListenerAdapter
中现有的支持已经允许你的方法具有非 void
返回类型。在这种情况下,调用的结果会封装在一个 jakarta.jms.Message
中,发送到原始消息的 JMSReplyTo
头中指定的目的地,或者发送到监听器上配置的默认目的地。你现在可以使用消息抽象的 @SendTo
注解来设置该默认目的地。
假设我们的 processOrder
方法现在应该返回一个 OrderStatus
,我们可以编写代码来自动发送响应,如下例所示:
@JmsListener(destination = "myDestination")
@SendTo("status")
public OrderStatus processOrder(Order order) {
// 订单处理
return status;
}
提示:如果你有多个带有 @JmsListener
注解的方法,你也可以将 @SendTo
注解放在类级别,以共享一个默认的回复目的地。
如果你需要以独立于传输的方式设置额外的头,你可以改为返回一个 Message
,方法类似如下:
@JmsListener(destination = "myDestination")
@SendTo("status")
public Message<OrderStatus> processOrder(Order order) {
// 订单处理
return MessageBuilder
.withPayload(status)
.setHeader("code", 1234)
.build();
}
如果你需要在运行时计算响应目的地,你可以将响应封装在一个 JmsResponse
实例中,该实例还提供了在运行时要使用的目的地。我们可以将上一个示例重写如下:
@JmsListener(destination = "myDestination")
public JmsResponse<Message<OrderStatus>> processOrder(Order order) {
// 订单处理
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;
}
}
# 9、JMS命名空间支持
Spring提供了一个XML命名空间,用于简化JMS配置。要使用JMS命名空间元素,你需要引用JMS模式,如下例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jms="http://www.springframework.org/schema/jms" <!-- (1) 引用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定义 -->
</beans>
该命名空间由三个顶级元素组成:<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
元素还可以包含几个可选属性。下表描述了所有可用的属性:
属性 | 描述 |
---|---|
id | 宿主监听器容器的bean名称。如果未指定,则会自动生成一个bean名称。 |
destination (必需) | 此监听器的目标名称,通过DestinationResolver 策略解析。 |
ref (必需) | 处理程序对象的bean名称。 |
method | 要调用的处理程序方法的名称。如果ref 属性指向一个MessageListener 或Spring的SessionAwareMessageListener ,则可以省略此属性。 |
response-destination | 默认响应目标的名称,用于发送响应消息。如果请求消息不包含JMSReplyTo 字段,则会使用此属性。此目标的类型由监听器容器的response-destination-type 属性决定。请注意,这仅适用于有返回值的监听器方法,每个结果对象都会被转换为响应消息。 |
subscription | 持久订阅的名称(如果有)。 |
selector | 此监听器的可选消息选择器。 |
concurrency | 为该监听器启动的并发会话或消费者的数量。此值可以是一个简单的数字,表示最大数量(例如5 ),也可以是一个范围,表示下限和上限(例如3 - 5 )。请注意,指定的最小值只是一个提示,可能会在运行时被忽略。默认值由容器提供。 |
<listener-container/>
元素也接受几个可选属性。这允许你自定义各种策略(例如taskExecutor
和destinationResolver
)以及基本的JMS设置和资源引用。通过使用这些属性,你可以定义高度定制的监听器容器,同时仍能受益于命名空间带来的便利。
你可以通过factory-id
属性指定要公开的bean的id
,将这些设置自动公开为JmsListenerContainerFactory
,如下例所示:
<jms:listener-container connection-factory="myConnectionFactory"
task-executor="myTaskExecutor"
destination-resolver="myDestinationResolver"
transaction-manager="myTransactionManager"
concurrency="10">
<jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/>
<jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/>
</jms:listener-container>
下表描述了所有可用的属性。有关各个属性的更多详细信息,请参阅AbstractMessageListenerContainer
(opens new window) 及其具体子类的类级别Javadoc。Javadoc还讨论了事务选择和消息重发场景。
属性 | 描述 |
---|---|
container-type | 此监听器容器的类型。可用选项为default 、simple 、default102 或simple102 (默认选项是default )。 |
container-class | 自定义监听器容器实现类的全限定类名。默认情况下,根据container-type 属性,使用Spring的标准DefaultMessageListenerContainer 或SimpleMessageListenerContainer 。 |
factory-id | 将此元素定义的设置公开为具有指定id 的JmsListenerContainerFactory ,以便可以与其他端点重用这些设置。 |
connection-factory | 对JMSConnectionFactory bean的引用(默认bean名称是connectionFactory )。 |
task-executor | 对SpringTaskExecutor 的引用,用于JMS监听器调用程序。 |
destination-resolver | 对DestinationResolver 策略的引用,用于解析JMSDestination 实例。 |
message-converter | 对MessageConverter 策略的引用,用于将JMS消息转换为监听器方法参数。默认是SimpleMessageConverter 。 |
error-handler | 对ErrorHandler 策略的引用,用于处理MessageListener 执行期间可能发生的任何未捕获异常。 |
destination-type | 此监听器的JMS目标类型:queue 、topic 、durableTopic 、sharedTopic 或sharedDurableTopic 。这可能会启用容器的pubSubDomain 、subscriptionDurable 和subscriptionShared 属性。默认是queue (这会禁用这三个属性)。 |
response-destination-type | 响应的JMS目标类型:queue 或topic 。默认值是destination-type 属性的值。 |
client-id | 此监听器容器的JMS客户端ID。使用持久订阅时必须指定该属性。 |
cache | JMS资源的缓存级别:none 、connection 、session 、consumer 或auto 。默认情况下(auto ),缓存级别实际上是consumer ,除非指定了外部事务管理器 — 在这种情况下,实际默认值将是none (假设使用Jakarta EE风格的事务管理,其中给定的ConnectionFactory 是一个支持XA的池)。 |
acknowledge | 原生JMS确认模式:auto 、client 、dups-ok 或transacted 。值为transacted 时会激活一个本地事务Session 。作为替代方案,你可以指定稍后在表中描述的transaction-manager 属性。默认值是auto 。 |
transaction-manager | 对外部PlatformTransactionManager 的引用(通常是基于XA的事务协调器,例如Spring的JtaTransactionManager )。如果未指定,则使用原生确认(请参阅acknowledge 属性)。 |
concurrency | 为每个监听器启动的并发会话或消费者的数量。它可以是一个简单的数字,表示最大数量(例如5 ),也可以是一个范围,表示下限和上限(例如3 - 5 )。请注意,指定的最小值只是一个提示,可能会在运行时被忽略。默认值是1 。对于主题监听器或队列排序很重要的情况,你应该将并发限制为1 。对于普通队列,可以考虑提高该值。 |
prefetch | 加载到单个会话中的最大消息数。请注意,提高此数字可能会导致并发消费者饥饿。 |
receive-timeout | 接收调用使用的超时时间(以毫秒为单位)。默认值是1000 (一秒)。-1 表示无超时。 |
back-off | 指定用于计算恢复尝试间隔的BackOff 实例。如果BackOffExecution 实现返回BackOffExecution#STOP ,则监听器容器将不再尝试恢复。设置此属性时,将忽略recovery-interval 值。默认是间隔为5000毫秒(即五秒)的FixedBackOff 。 |
recovery-interval | 指定恢复尝试之间的间隔(以毫秒为单位)。它提供了一种方便的方式来创建具有指定间隔的FixedBackOff 。如需更多恢复选项,请考虑指定BackOff 实例。默认值是5000毫秒(即五秒)。 |
phase | 此容器应启动和停止的生命周期阶段。值越低,此容器启动越早,停止越晚。默认值是Integer.MAX_VALUE ,这意味着容器尽可能晚地启动,尽可能早地停止。 |
使用jms
模式支持配置基于JCA的监听器容器非常类似,如下例所示:
<jms:jca-listener-container resource-adapter="myResourceAdapter"
destination-resolver="myDestinationResolver"
transaction-manager="myTransactionManager"
concurrency="10">
<jms:listener destination="queue.orders" ref="myMessageListener"/>
</jms:jca-listener-container>
下表描述了JCA变体的可用配置选项:
属性 | 描述 |
---|---|
factory-id | 将此元素定义的设置公开为具有指定id 的JmsListenerContainerFactory ,以便可以与其他端点重用这些设置。 |
resource-adapter | 对JCAResourceAdapter bean的引用(默认bean名称是resourceAdapter )。 |
activation-spec-factory | 对JmsActivationSpecFactory 的引用。默认情况下,会自动检测JMS提供程序及其ActivationSpec 类(请参阅DefaultJmsActivationSpecFactory (opens new window))。 |
destination-resolver | 对DestinationResolver 策略的引用,用于解析JMSDestination 。 |
message-converter | 对MessageConverter 策略的引用,用于将JMS消息转换为监听器方法参数。默认是SimpleMessageConverter 。 |
destination-type | 此监听器的JMS目标类型:queue 、topic 、durableTopic 、sharedTopic 或sharedDurableTopic 。这可能会启用容器的pubSubDomain 、subscriptionDurable 和subscriptionShared 属性。默认是queue (这会禁用这三个属性)。 |
response-destination-type | 响应的JMS目标类型:queue 或topic 。默认值是destination-type 属性的值。 |
client-id | 此监听器容器的JMS客户端ID。使用持久订阅时需要指定该属性。 |
acknowledge | 原生JMS确认模式:auto 、client 、dups-ok 或transacted 。值为transacted 时会激活一个本地事务Session 。作为替代方案,你可以指定稍后描述的transaction-manager 属性。默认值是auto 。 |
transaction-manager | 对SpringJtaTransactionManager 或jakarta.transaction.TransactionManager 的引用,用于为每个传入消息启动XA事务。如果未指定,则使用原生确认(请参阅acknowledge 属性)。 |
concurrency | 为每个监听器启动的并发会话或消费者的数量。它可以是一个简单的数字,表示最大数量(例如5 ),也可以是一个范围,表示下限和上限(例如3 - 5 )。请注意,指定的最小值只是一个提示,在使用JCA监听器容器时,通常会在运行时被忽略。默认值是1。 |
prefetch | 加载到单个会话中的最大消息数。请注意,提高此数字可能会导致并发消费者饥饿。 |
# 三、JMX
Spring 中的 JMX(Java 管理扩展)支持提供了相关特性,能让你轻松且透明地将 Spring 应用集成到 JMX 基础设施中。
# 1、是什么?
本章并非 JMX 的入门介绍,也不会解释你为何要使用 JMX。如果你对 JMX 还不熟悉,请参阅本章末尾的更多资源。
具体而言,Spring 的 JMX 支持提供了四大核心特性:
- 自动将任何 Spring Bean 注册为 JMX MBean。
- 拥有灵活的机制来控制 Bean 的管理接口。
- 通过远程 JSR - 160 连接器以声明式方式暴露 MBean。
- 能简单地代理本地和远程的 MBean 资源。
这些特性在设计上不会让应用组件与 Spring 或 JMX 的接口和类产生耦合。实际上,在大多数情况下,你的应用类无需了解 Spring 或 JMX 也能利用 Spring JMX 特性。
# 2、将 Bean 导出到 JMX
Spring 的 JMX 框架的核心类是 MBeanExporter
。这个类负责将你的 Spring Bean 注册到 JMX MBeanServer
中。例如,考虑以下类:
public class JmxTestBean implements IJmxTestBean {
private String name;
private int age;
@Override
public int getAge() {
return age;
}
@Override
public void setAge(int age) {
this.age = age;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public int add(int x, int y) {
return x + y;
}
@Override
public void dontExposeMe() {
throw new RuntimeException();
}
}
要将这个 Bean 的属性和方法作为 MBean 的属性和操作公开,可以在配置文件中配置 MBeanExporter
类的一个实例,并传入该 Bean,如下例所示:
@Configuration
public class JmxConfiguration {
@Bean
MBeanExporter exporter(JmxTestBean testBean) {
MBeanExporter exporter = new MBeanExporter();
exporter.setBeans(Map.of("bean:name=testBean1", testBean));
return exporter;
}
@Bean
JmxTestBean testBean() {
JmxTestBean testBean = new JmxTestBean();
testBean.setName("TEST");
testBean.setAge(100);
return testBean;
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 如果要进行导出操作,这个 Bean 不能被延迟初始化 -->
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean"/>
</map>
</property>
</bean>
<bean id="testBean" class="org.example.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
上述配置片段中的相关 Bean 定义是 exporter
Bean。beans
属性告知 MBeanExporter
确切要将哪些 Bean 导出到 JMX MBeanServer
中。在默认配置下,beans
Map
中每个条目的键用作对应条目的值所引用的 Bean 的 ObjectName
。你可以按照控制 Bean 的 ObjectName
实例中所述来更改此行为。
通过此配置,testBean
Bean 以 ObjectName
bean:name=testBean1
作为 MBean 公开。默认情况下,Bean 的所有 public
属性作为属性公开,所有 public
方法(继承自 Object
类的方法除外)作为操作公开。
注意:MBeanExporter
是一个 Lifecycle
Bean(请参阅启动和关闭回调)。默认情况下,MBean 在应用程序生命周期中尽可能晚地导出。你可以配置导出发生的阶段,或者通过设置 autoStartup
标志来禁用自动注册。
# 3、创建 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"/>
<!--
为了进行导出操作,这个 Bean 需要提前实例化;
这意味着它不能被标记为延迟初始化
-->
<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>
在上述示例中,MBeanServerFactoryBean
创建了一个 MBeanServer
实例,并通过 server
属性将其提供给 MBeanExporter
。当你提供自己的 MBeanServer
实例时,MBeanExporter
不会尝试定位正在运行的 MBeanServer
,而是使用提供的 MBeanServer
实例。为了使其正常工作,你的类路径中必须有一个 JMX 实现。
# 4、重用现有的 MBeanServer
如果未指定服务器,MBeanExporter
会尝试自动检测正在运行的 MBeanServer
。这在大多数仅使用一个 MBeanServer
实例的环境中都有效。但是,当存在多个实例时,导出器可能会选择错误的服务器。在这种情况下,你应该使用 MBeanServer
的 agentId
来指示要使用的实例,如下例所示:
<beans>
<bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
<!-- 指示首先查找服务器 -->
<property name="locateExistingServerIfPossible" value="true"/>
<!-- 搜索具有给定 agentId 的 MBeanServer 实例 -->
<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
且该 agentId
通过查找方法获取的平台或情况,你应该使用工厂方法,如下例所示:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="server">
<!-- 自定义 MBeanServerLocator -->
<bean class="platform.package.MBeanServerLocator" factory-method="locateMBeanServer"/>
</property>
</bean>
<!-- 其他 Bean 在此处 -->
</beans>
# 5、延迟初始化的 MBean
如果你配置了一个与 MBeanExporter
关联的 Bean,并且该 Bean 也配置为延迟初始化,MBeanExporter
不会破坏此约定,并且会避免实例化该 Bean。相反,它会向 MBeanServer
注册一个代理,并将从容器获取 Bean 的操作推迟到代理的第一次调用发生时。
这也会影响 FactoryBean
的解析,其中 MBeanExporter
会定期内省所生成的对象,从而有效地触发 FactoryBean.getObject()
。为避免这种情况,请将相应的 Bean 定义标记为延迟初始化。
# 6、的自动注册
通过 MBeanExporter
导出且已经是有效 MBean 的任何 Bean,都会直接注册到 MBeanServer
中,而无需 Spring 进一步干预。你可以通过将 autodetect
属性设置为 true
,让 MBeanExporter
自动检测 MBean,如下例所示:
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="autodetect" value="true"/>
</bean>
<bean name="spring:mbean=true" class="org.springframework.jmx.export.TestDynamicMBean"/>
在上述示例中,名为 spring:mbean=true
的 Bean 已经是一个有效的 JMX MBean,并由 Spring 自动注册。默认情况下,为 JMX 注册自动检测到的 Bean 使用其 Bean 名称作为 ObjectName
。你可以按照控制 Bean 的 ObjectName
实例中所述来覆盖此行为。
# 7、控制注册行为
考虑这样一种场景:Spring MBeanExporter
尝试使用 ObjectName
bean:name=testBean1
将一个 MBean
注册到 MBeanServer
中。如果已经有一个 MBean
实例使用相同的 ObjectName
进行了注册,默认行为是失败(并抛出一个 InstanceAlreadyExistsException
)。
你可以精确控制将 MBean
注册到 MBeanServer
时发生的情况。Spring 的 JMX 支持提供了三种不同的注册行为,用于在注册过程中发现已经有一个 MBean
使用相同的 ObjectName
注册时控制注册行为。下表总结了这些注册行为:
注册行为 | 解释 |
---|---|
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>
# 8、控制 Bean 的管理接口
在上一节的示例中,你对 Bean 的管理接口控制能力有限。每个导出 Bean 的所有公共属性和方法分别作为 JMX 属性和操作暴露出来。为了更精细地控制导出 Bean 的哪些属性和方法实际作为 JMX 属性和操作暴露,Spring JMX 提供了一个全面且可扩展的机制来控制 Bean 的管理接口。
# 8.1、使用 MBeanInfoAssembler
API
实际上,MBeanExporter
会委托给 org.springframework.jmx.export.assembler.MBeanInfoAssembler
API 的一个实现,该实现负责定义每个暴露 Bean 的管理接口。默认实现 org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler
定义的管理接口会暴露所有公共属性和方法(正如你在前面章节的示例中看到的那样)。Spring 还提供了 MBeanInfoAssembler
接口的另外两个实现,让你可以通过使用源码级元数据或任意接口来控制生成的管理接口。
# 8.2、使用源码级元数据:Java 注解
通过使用 MetadataMBeanInfoAssembler
,你可以使用源码级元数据为 Bean 定义管理接口。元数据的读取由 org.springframework.jmx.export.metadata.JmxAttributeSource
接口封装。Spring JMX 提供了一个使用 Java 注解的默认实现,即 org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource
。你必须为 MetadataMBeanInfoAssembler
配置一个 JmxAttributeSource
接口的实现实例,它才能正常工作,因为没有默认配置。
要将一个 Bean 导出到 JMX,你应该用 @ManagedResource
注解标注 Bean 类。你想要作为操作暴露的每个方法都应该用 @ManagedOperation
注解标注,想要暴露的每个属性都应该用 @ManagedAttribute
注解标注。在标注属性时,如果你省略 getter 或 setter 方法的注解,相应地会创建一个只写或只读属性。
注意:用 @ManagedResource
注解标注的 Bean 及其暴露操作或属性的方法都必须是公共的。
以下示例展示了我们在“创建 MBeanServer”中使用的 JmxTestBean
类的注解版本:
package org.springframework.jmx;
@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 {
private int age;
private String name;
public void setAge(int age) {
this.age = age;
}
// 标记为 JMX 管理属性,提供描述和有效时间限制
@ManagedAttribute(description="The Age Attribute", currencyTimeLimit=15)
public int getAge() {
return this.age;
}
// 标记为 JMX 管理属性,提供描述、有效时间限制、默认值和持久化策略
@ManagedAttribute(description="The Name Attribute",
currencyTimeLimit=20,
defaultValue="bar",
persistPolicy="OnUpdate")
public void setName(String name) {
this.name = name;
}
// 标记为 JMX 管理属性,提供默认值和持久化周期
@ManagedAttribute(defaultValue="foo", persistPeriod=300)
public String getName() {
return this.name;
}
// 标记为 JMX 管理操作,并为参数提供描述
@ManagedOperation(description="Add two numbers")
@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();
}
}
在上述示例中,你可以看到 AnnotationTestBean
类使用 @ManagedResource
注解标注,并且该注解配置了一组属性。这些属性可用于配置 MBeanExporter
生成的 MBean 的各个方面,详情将在“Spring JMX 注解”中进一步解释。
age
和 name
属性都用 @ManagedAttribute
注解标注,但对于 age
属性,只标注了 getter 方法。这使得这两个属性都作为管理属性包含在管理接口中,但 age
属性是只读的。
最后,add(int, int)
方法用 @ManagedOperation
注解标注,而 dontExposeMe()
方法没有标注。当使用 MetadataMBeanInfoAssembler
时,这使得管理接口只包含一个操作(add(int, int)
)。
注意:AnnotationTestBean
类不需要实现任何 Java 接口,因为 JMX 管理接口完全从注解中推导得出。
以下配置展示了如何配置 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="assembler"
class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
<property name="attributeSource" ref="jmxAttributeSource"/>
</bean>
<!-- 从注解中获取 ObjectName -->
<bean id="namingStrategy"
class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
<property name="attributeSource" ref="jmxAttributeSource"/>
</bean>
<bean id="jmxAttributeSource"
class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>
<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 利用注解驱动的管理接口所需的全部配置。
# 8.3、JMX 注解
下表描述了可在 Spring JMX 中使用的注解:
注解 | 应用于 | 描述 |
---|---|---|
@ManagedResource | 类 | 将 Class 的所有实例标记为 JMX 管理资源。 |
@ManagedNotification | 类 | 表示管理资源发出的 JMX 通知。 |
@ManagedAttribute | 方法(仅 getter 和 setter) | 将 getter 或 setter 标记为 JMX 属性的一部分。 |
@ManagedMetric | 方法(仅 getter) | 将 getter 标记为 JMX 属性,并添加描述符属性以表明它是一个指标。 |
@ManagedOperation | 方法 | 将方法标记为 JMX 操作。 |
@ManagedOperationParameter | 方法 | 定义操作参数的描述。 |
下表描述了这些注解中可用的一些常见属性。有关详细信息,请参考每个注解的 Javadoc。
属性 | 应用于 | 描述 |
---|---|---|
objectName | @ManagedResource | 供 MetadataNamingStrategy 确定管理资源的 ObjectName 。 |
description | @ManagedResource 、@ManagedNotification 、@ManagedAttribute 、@ManagedMetric 、@ManagedOperation 、@ManagedOperationParameter | 设置资源、通知、属性、指标或操作的描述。 |
currencyTimeLimit | @ManagedResource 、@ManagedAttribute 、@ManagedMetric | 设置 currencyTimeLimit 描述符字段的值。 |
defaultValue | @ManagedAttribute | 设置 defaultValue 描述符字段的值。 |
log | @ManagedResource | 设置 log 描述符字段的值。 |
logFile | @ManagedResource | 设置 logFile 描述符字段的值。 |
persistPolicy | @ManagedResource 、@ManagedMetric | 设置 persistPolicy 描述符字段的值。 |
persistPeriod | @ManagedResource 、@ManagedMetric | 设置 persistPeriod 描述符字段的值。 |
persistLocation | @ManagedResource | 设置 persistLocation 描述符字段的值。 |
persistName | @ManagedResource | 设置 persistName 描述符字段的值。 |
name | @ManagedOperationParameter | 设置操作参数的显示名称。 |
index | @ManagedOperationParameter | 设置操作参数的索引。 |
# 8.4、使用 AutodetectCapableMBeanInfoAssembler
接口
为了进一步简化配置,Spring 包含了 AutodetectCapableMBeanInfoAssembler
接口,它扩展了 MBeanInfoAssembler
接口,以增加对 MBean 资源自动检测的支持。如果你用 AutodetectCapableMBeanInfoAssembler
的一个实例配置 MBeanExporter
,它可以对包含要暴露给 JMX 的 Bean 进行“投票”。
AutodetectCapableMBeanInfo
接口的唯一实现是 MetadataMBeanInfoAssembler
,它会投票包含任何标记有 ManagedResource
属性的 Bean。在这种情况下,默认方法是使用 Bean 名称作为 ObjectName
,这会得到类似以下的配置:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<!-- 注意这里没有显式配置 'beans' -->
<property name="autodetect" value="true"/>
<property name="assembler" ref="assembler"/>
</bean>
<bean id="assembler" class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
<property name="attributeSource">
<bean class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>
</property>
</bean>
<bean id="testBean" class="org.springframework.jmx.AnnotationTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
注意,在上述配置中,没有将任何 Bean 传递给 MBeanExporter
。然而,AnnotationTestBean
仍然被注册,因为它用 @ManagedResource
注解标注,并且 MetadataMBeanInfoAssembler
检测到这一点并投票包含它。这种方法的唯一缺点是 AnnotationTestBean
的名称现在具有业务含义。你可以通过配置 ObjectNamingStrategy
来解决这个问题,详情见“控制 Bean 的 ObjectName
实例”。你也可以在“使用源码级元数据:Java 注解”中看到使用 MetadataNamingStrategy
的示例。
# 8.5、使用 Java 接口定义管理接口
除了 MetadataMBeanInfoAssembler
,Spring 还包含 InterfaceBasedMBeanInfoAssembler
,它允许你根据一组接口中定义的方法来限制暴露的方法和属性。
虽然暴露 MBean 的标准机制是使用接口和简单的命名方案,但 InterfaceBasedMBeanInfoAssembler
扩展了此功能,消除了对命名约定的需求,允许你使用多个接口,并且不需要 Bean 实现 MBean 接口。
考虑以下接口,它用于为我们之前展示的 JmxTestBean
类定义管理接口:
public interface IJmxTestBean {
public int add(int x, int y);
public long myOperation();
public int getAge();
public void setAge(int age);
public void setName(String name);
public String getName();
}
此接口定义了作为 JMX MBean 上的操作和属性暴露的方法和属性。以下代码展示了如何配置 Spring JMX 使用此接口作为管理接口的定义:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean5" value-ref="testBean"/>
</map>
</property>
<property name="assembler">
<bean class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler">
<property name="managedInterfaces">
<value>org.springframework.jmx.IJmxTestBean</value>
</property>
</bean>
</property>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
</beans>
在上述示例中,InterfaceBasedMBeanInfoAssembler
配置为在为任何 Bean 构建管理接口时使用 IJmxTestBean
接口。重要的是要理解,由 InterfaceBasedMBeanInfoAssembler
处理的 Bean 不需要实现用于生成 JMX 管理接口的接口。
在上述情况下,IJmxTestBean
接口用于为所有 Bean 构建所有管理接口。在许多情况下,这不是所需的行为,你可能希望为不同的 Bean 使用不同的接口。在这种情况下,你可以通过 interfaceMappings
属性将一个 Properties
实例传递给 InterfaceBasedMBeanInfoAssembler
,其中每个条目的键是 Bean 名称,每个条目的值是用于该 Bean 的接口名称的逗号分隔列表。
如果没有通过 managedInterfaces
或 interfaceMappings
属性指定管理接口,InterfaceBasedMBeanInfoAssembler
将对 Bean 进行反射,并使用该 Bean 实现的所有接口来创建管理接口。
# 8.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>
在上述示例中,你可以看到 add
和 myOperation
方法作为 JMX 操作暴露,getName()
、setName(String)
和 getAge()
作为 JMX 属性的相应部分暴露。在上述代码中,方法映射适用于暴露给 JMX 的 Bean。要逐 Bean 地控制方法暴露,你可以使用 MethodNameMBeanInfoAssembler
的 methodMappings
属性将 Bean 名称映射到方法名称列表。
# 9、控制 Bean 的 ObjectName
实例
在幕后,MBeanExporter
会委托给 ObjectNamingStrategy
的一个实现,为其注册的每个 bean 获取一个 ObjectName
实例。默认情况下,默认实现 KeyNamingStrategy
会使用 beans
映射(Map
)中的键作为 ObjectName
。此外,KeyNamingStrategy
还可以将 beans
映射的键映射到一个(或多个)Properties
文件中的条目,以解析出 ObjectName
。除了 KeyNamingStrategy
之外,Spring 还提供了另外两种 ObjectNamingStrategy
实现:IdentityNamingStrategy
(根据 bean 的 JVM 标识构建 ObjectName
)和 MetadataNamingStrategy
(使用源代码级别的元数据来获取 ObjectName
)。
# 9.1、从属性文件读取 ObjectName
实例
你可以配置自己的 KeyNamingStrategy
实例,并将其配置为从一个 Properties
实例中读取 ObjectName
实例,而不是使用 bean 的键。KeyNamingStrategy
会尝试在 Properties
中找到与 bean 键对应的条目。如果未找到对应条目,或者 Properties
实例为 null
,则会直接使用 bean 的键。
以下代码展示了 KeyNamingStrategy
的示例配置:
<beans>
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="testBean" value-ref="testBean"/>
</map>
</property>
<property name="namingStrategy" ref="namingStrategy"/>
</bean>
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
<bean id="namingStrategy" class="org.springframework.jmx.export.naming.KeyNamingStrategy">
<property name="mappings">
<props>
<prop key="testBean">bean:name=testBean1</prop>
</props>
</property>
<property name="mappingLocations">
<value>names1.properties,names2.properties</value>
</property>
</bean>
</beans>
上述示例配置了一个 KeyNamingStrategy
实例,其 Properties
实例是由 mapping
属性定义的 Properties
实例和 mappingLocations
属性指定路径下的属性文件合并而成的。在这个配置中,testBean
这个 bean 的 ObjectName
被指定为 bean:name=testBean1
,因为在 Properties
实例中有一个键与该 bean 的键相对应的条目。
如果在 Properties
实例中找不到对应条目,那么 bean 的键名将被用作 ObjectName
。
# 9.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=[短类名],name=[bean 名]。例如,对于以下 bean,生成的 ObjectName
将是 com.example:type=MyClass,name=myBean
:
<bean id="myBean" class="com.example.MyClass"/>
# 9.3、配置基于注解的 MBean 导出
如果你倾向于使用基于注解的方式来定义管理接口,那么可以使用 MBeanExporter
的一个便捷子类:AnnotationMBeanExporter
。在定义这个子类的实例时,你不再需要配置 namingStrategy
、assembler
和 attributeSource
,因为它始终使用标准的基于 Java 注解的元数据(并且始终启用自动检测)。事实上,与定义一个 MBeanExporter
bean 相比,使用 @EnableMBeanExport
@Configuration
注解或 <context:mbean-export/>
元素支持更简单的语法,如下例所示:
Java 配置
@Configuration
@EnableMBeanExport
public class JmxConfiguration {
}
XML 配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:mbean-export/>
</beans>
如果需要,你可以提供一个特定 MBean server
的引用,并且 defaultDomain
属性(AnnotationMBeanExporter
的一个属性)可以接受一个替代值,用于生成的 MBean ObjectName
域名。如前一节 MetadataNamingStrategy 所述,该值会替代全限定包名,如下例所示:
Java 配置
@Configuration
@EnableMBeanExport(server="myMBeanServer", defaultDomain="myDomain")
public class CustomJmxConfiguration {
}
XML 配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:mbean-export server="myMBeanServer" default-domain="myDomain"/>
</beans>
注意:请勿将基于接口的 AOP 代理与在 bean 类中自动检测 JMX 注解结合使用。基于接口的代理会 “隐藏” 目标类,这也会隐藏 JMX 管理资源注解。因此,在这种情况下,你应该使用基于目标类的代理(通过在 <aop:config/>
、<tx:annotation-driven/>
等标签上设置 proxy-target-class
标志)。否则,你的 JMX bean 可能在启动时被无声地忽略。
# 10、使用 JSR - 160 连接器
对于远程访问,Spring JMX 模块在 org.springframework.jmx.support
包中提供了两个 FactoryBean
实现,用于创建服务器端和客户端连接器。
# 10.1、服务器端连接器
若要让 Spring JMX 创建、启动并暴露一个 JSR - 160 的 JMXConnectorServer
,可以使用以下配置:
<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean"/>
默认情况下,ConnectorServerFactoryBean
会创建一个绑定到 service:jmx:jmxmp://localhost:9875
的 JMXConnectorServer
。因此,serverConnector
这个 Bean 通过 JMXMP 协议在本地主机的 9875 端口将本地的 MBeanServer
暴露给客户端。需要注意的是,JSR 160 规范将 JMXMP 协议标记为可选。目前,主流的开源 JMX 实现(如 MX4J)和 JDK 自带的实现都不支持 JMXMP。
若要指定另一个 URL 并将 JMXConnectorServer
本身注册到 MBeanServer
中,可以分别使用 serviceUrl
和 ObjectName
属性,如下例所示:
<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 的连接器时,需要启动查找服务(tnameserv
或 rmiregistry
)才能完成名称注册。
# 10.2、客户端连接器
若要创建一个到支持 JSR - 160 的远程 MBeanServer
的 MBeanServerConnection
,可以使用 MBeanServerConnectionFactoryBean
,如下例所示:
<bean id="clientConnector" class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
<property name="serviceUrl" value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"/>
</bean>
# 10.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 的官方文档。
# 11、通过代理访问 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>
在上述示例中,可以看到为注册在 ObjectName
为 bean:name=testBean
的 MBean 创建了一个代理。代理实现的接口集由 proxyInterfaces
属性控制,并且将这些接口上的方法和属性映射到 MBean 上的操作和属性的规则,与 InterfaceBasedMBeanInfoAssembler
使用的规则相同。
MBeanProxyFactoryBean
可以创建一个代理,以访问任何可通过 MBeanServerConnection
访问的 MBean。默认情况下,会定位并使用本地 MBeanServer
,但你可以覆盖此设置,并提供一个指向远程 MBeanServer
的 MBeanServerConnection
,以便为指向远程 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>
在上述示例中,我们使用 MBeanServerConnectionFactoryBean
创建了一个指向远程机器的 MBeanServerConnection
。然后,通过 server
属性将这个 MBeanServerConnection
传递给 MBeanProxyFactoryBean
。创建的代理会通过这个 MBeanServerConnection
将所有调用转发到 MBeanServer
。
# 12、通知
Spring 的 JMX 功能提供了对 JMX 通知的全面支持。
# 12.1、注册通知监听器
Spring 的 JMX 支持能让你轻松地为任意数量的 MBean 注册任意数量的 NotificationListener
(这其中包含通过 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 name="notificationListenerMappings">
<map>
<entry key="*">
<bean class="com.example.ConsoleLoggingNotificationListener"/>
</entry>
</map>
</property>
如果你需要做相反的操作(即,为一个 MBean 注册多个不同的监听器),则必须使用 notificationListeners
列表属性(优先于 notificationListenerMappings
属性)。这次,我们不是为单个 MBean 配置 NotificationListener
,而是配置 NotificationListenerBean
实例。NotificationListenerBean
封装了一个 NotificationListener
以及要在 MBeanServer
中针对其进行注册的 ObjectName
(或 ObjectNames
)。NotificationListenerBean
还封装了许多其他属性,例如 NotificationFilter
和一个任意的回传对象,这些在高级 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
时我们都希望获得一个回传对象,并且也希望通过提供一个 NotificationFilter
来过滤掉不必要的 Notification
。下面的示例可以实现这些目标:
<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>
<!-- 处理来自两个不同 MBean 的通知 -->
<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>
<!-- 同时实现 NotificationListener 和 NotificationFilter 接口 -->
<bean id="customerNotificationListener" class="com.example.ConsoleLoggingNotificationListener"/>
<bean id="testBean1" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
<bean id="testBean2" class="org.springframework.jmx.JmxTestBean">
<property name="name" value="ANOTHER TEST"/>
<property name="age" value="200"/>
</bean>
</beans>
(有关回传对象和 NotificationFilter
的完整讨论,请参阅 JMX 规范(1.2)中名为“JMX 通知模型”的部分。)
# 12.2、发布通知
Spring 不仅支持注册接收 Notification
,还支持发布 Notification
。
注意:本节内容实际上仅适用于通过 MBeanExporter
作为 MBean 公开的 Spring 管理的 bean。任何现有的用户自定义 MBean 都应该使用标准的 JMX API 进行通知发布。
Spring 的 JMX 通知发布支持中的关键接口是 NotificationPublisher
接口(定义在 org.springframework.jmx.export.notification
包中)。任何要通过 MBeanExporter
实例导出为 MBean 的 bean 都可以实现相关的 NotificationPublisherAware
接口来获取 NotificationPublisher
实例。NotificationPublisherAware
接口通过一个简单的 setter 方法向实现该接口的 bean 提供一个 NotificationPublisher
实例,然后该 bean 可以使用该实例发布 Notification
。
正如 NotificationPublisher (opens new window) 接口的 Javadoc 中所述,通过 NotificationPublisher
机制发布事件的托管 bean 无需负责管理通知监听器的状态。Spring 的 JMX 支持会处理所有的 JMX 基础架构问题。作为应用程序开发人员,你只需实现 NotificationPublisherAware
接口,然后使用提供的 NotificationPublisher
实例开始发布事件即可。请注意,NotificationPublisher
是在托管 bean 注册到 MBeanServer
之后设置的。
使用 NotificationPublisher
实例非常简单。你创建一个 JMX 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;
// 为清晰起见,省略了其他 getter 和 setter 方法
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 的耦合,那么就可以使用它。
# 13、更多资源
本节提供了有关 JMX 的更多资源链接:
- Oracle 上的 JMX 主页 (opens new window)。
- JMX 规范 (opens new window)(JSR - 000003)。
- JMX 远程 API 规范 (opens new window)(JSR - 000160)。
- MX4J 主页。(MX4J 是各种 JMX 规范的开源实现。)
# 四、电子邮件
本节介绍如何使用 Spring 框架发送电子邮件。
# 1、库依赖
为了使用 Spring 框架的电子邮件支持,您的应用程序类路径中需要包含以下 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
类是一个简单的值对象,用于封装简单邮件的属性,如发件人(from
)和收件人(to
)等。该包还包含一系列经过检查的异常,这些异常为底层邮件系统异常提供了更高级别的抽象,根异常是 MailException
。更多关于丰富的邮件异常层次结构的信息,请参阅 javadoc (opens new window)。
org.springframework.mail.javamail.JavaMailSender
接口在 MailSender
接口(它继承自该接口)的基础上添加了专门的 JavaMail 特性,例如 MIME 消息支持。JavaMailSender
还提供了一个回调接口 org.springframework.mail.javamail.MimeMessagePreparator
,用于准备 MimeMessage
。
# 2、使用方法
假设我们有一个名为 OrderManager
的业务接口,如下所示:
public interface OrderManager {
void placeOrder(Order order);
}
进一步假设我们有一个需求,即需要生成包含订单编号的电子邮件消息,并将其发送给下了相关订单的客户。
# 2.1、基本的 MailSender
和 SimpleMailMessage
使用方法
以下示例展示了如何在有人下单时使用 MailSender
和 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;
}
@Override
public void placeOrder(Order order) {
// 进行业务计算...
// 调用协作类来持久化订单...
// 创建模板消息的线程安全“副本”并进行定制
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) {
// 简单记录日志并继续...
System.err.println(ex.getMessage());
}
}
}
以下是上述代码的 Bean 定义示例:
@Bean
JavaMailSender mailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("mail.mycompany.example");
return mailSender;
}
@Bean // 这是一个可以预加载默认状态的模板消息
SimpleMailMessage templateMessage() {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("[email protected]");
message.setSubject("Your order");
return message;
}
@Bean
SimpleOrderManager orderManager(JavaMailSender mailSender, SimpleMailMessage templateMessage) {
SimpleOrderManager orderManager = new SimpleOrderManager();
orderManager.setMailSender(mailSender);
orderManager.setTemplateMessage(templateMessage);
return orderManager;
}
XML 配置方式如下:
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="mail.mycompany.example"/>
</bean>
<!-- 这是一个可以预加载默认状态的模板消息 -->
<bean id="templateMessage" class="org.springframework.mail.SimpleMailMessage">
<property name="from" value="[email protected]"/>
<property name="subject" value="Your order"/>
</bean>
<bean id="orderManager" class="com.mycompany.businessapp.support.SimpleOrderManager">
<property name="mailSender" ref="mailSender"/>
<property name="templateMessage" ref="templateMessage"/>
</bean>
# 2.2、使用 JavaMailSender
和 MimeMessagePreparator
本节介绍 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 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) {
// 进行业务计算...
// 调用协作类来持久化订单...
MimeMessagePreparator preparator = new MimeMessagePreparator() {
public void prepare(MimeMessage mimeMessage) throws Exception {
mimeMessage.setRecipient(Message.RecipientType.TO,
new InternetAddress(order.getCustomer().getEmailAddress()));
mimeMessage.setFrom(new InternetAddress("[email protected]"));
mimeMessage.setText("Dear " + order.getCustomer().getFirstName() + " " +
order.getCustomer().getLastName() + ", thanks for your order. " +
"Your order number is " + order.getOrderNumber() + ".");
}
};
try {
this.mailSender.send(preparator);
} catch (MailException ex) {
// 简单记录日志并继续...
System.err.println(ex.getMessage());
}
}
}
注意:邮件代码属于横切关注点,可以考虑将其重构为一个自定义 Spring AOP 切面,这样就可以在 OrderManager
目标的合适连接点上执行。
Spring 框架的邮件支持基于标准的 JavaMail 实现。更多信息请参阅相关的 javadoc。
# 3、使用 JavaMail 的 MimeMessageHelper
在处理 JavaMail 消息时,org.springframework.mail.javamail.MimeMessageHelper
类非常实用,它可以让您避免使用冗长的 JavaMail API。使用 MimeMessageHelper
创建 MimeMessage
非常简单,以下是一个示例:
// 在实际应用中,您当然会使用依赖注入(DI)
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");
MimeMessage message = sender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setTo("[email protected]");
helper.setText("Thank you for ordering!");
sender.send(message);
# 3.1、发送附件和内联资源
多部分电子邮件消息允许包含附件和内联资源。内联资源的示例包括您想在邮件中使用但又不想作为附件显示的图片或样式表。
# a、附件
以下示例展示了如何使用 MimeMessageHelper
发送包含单个 JPEG 图片附件的电子邮件:
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");
MimeMessage message = sender.createMimeMessage();
// 使用 true 标志表示需要多部分消息
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("[email protected]");
helper.setText("Check out this image!");
// 附加 Windows 的示例文件(这里假设已复制到 c:/ 目录)
FileSystemResource file = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addAttachment("CoolImage.jpg", file);
sender.send(message);
# b、内联资源
以下示例展示了如何使用 MimeMessageHelper
发送包含内联图片的电子邮件:
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");
MimeMessage message = sender.createMimeMessage();
// 使用 true 标志表示需要多部分消息
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("[email protected]");
// 使用 true 标志表示包含的文本是 HTML
helper.setText("<html><body><img src='cid:identifier1234'></body></html>", true);
// 包含 Windows 的示例文件(这里假设已复制到 c:/ 目录)
FileSystemResource res = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addInline("identifier1234", res);
sender.send(message);
警告:内联资源通过指定的 Content-ID
(上述示例中的 identifier1234
)添加到 MimeMessage
中。添加文本和资源的顺序非常重要。请确保先添加文本,再添加资源。如果顺序相反,可能无法正常工作。
# 3.2、使用模板库创建电子邮件内容
前面示例中的代码通过调用 message.setText(..)
等方法显式地创建了电子邮件消息的内容。对于简单的情况,这样做是可以的,而且在上述示例中,其目的是向您展示 API 的基本用法。
但是,在典型的企业应用程序中,开发人员通常不会使用上述方法来创建电子邮件消息的内容,原因如下:
- 在 Java 代码中创建基于 HTML 的电子邮件内容既繁琐又容易出错。
- 显示逻辑和业务逻辑没有明确的分离。
- 更改电子邮件内容的显示结构需要编写 Java 代码、重新编译、重新部署等。
通常,解决这些问题的方法是使用模板库(如 FreeMarker)来定义电子邮件内容的显示结构。这样,您的代码只需负责创建要在电子邮件模板中呈现的数据并发送邮件。当您的电子邮件消息内容变得稍微复杂时,这绝对是一种最佳实践,而且借助 Spring 框架对 FreeMarker 的支持类,实现起来非常容易。
# 五、任务执行与调度
Spring 框架分别通过 TaskExecutor
和 TaskScheduler
接口为任务的异步执行和调度提供了抽象。Spring 还提供了这些接口的实现,支持在线程池或在应用服务器环境中委托给 CommonJ 。最终,在通用接口背后使用这些实现,抽象出了 Java SE 和 Jakarta EE 环境之间的差异。
Spring 还提供了集成类,以支持使用 Quartz 调度器 (opens new window)进行调度。
# 1、TaskExecutor
抽象
执行器(Executors)是 JDK 对线程池概念的命名。之所以称为“执行器”,是因为不能保证底层实现实际上是一个池。执行器可以是单线程的,甚至是同步的。Spring 的抽象隐藏了 Java SE 和 Jakarta EE 环境之间的实现细节。
Spring 的 TaskExecutor
接口与 java.util.concurrent.Executor
接口相同。实际上,它最初存在的主要原因是在使用线程池时无需依赖 Java 5 。该接口有一个单一的方法 (execute(Runnable task)
),它根据线程池的语义和配置接受一个任务进行执行。
TaskExecutor
最初是为了给其他 Spring 组件在需要线程池的地方提供一个抽象而创建的。像 ApplicationEventMulticaster
、JMS 的 AbstractMessageListenerContainer
和 Quartz 集成等组件都使用 TaskExecutor
抽象来管理线程池。不过,如果你的 Bean 需要线程池行为,你也可以出于自己的需求使用这个抽象。
# 1.1、TaskExecutor
类型
Spring 包含了许多预构建的 TaskExecutor
实现。很可能你永远不需要自己实现它。Spring 提供的变体如下:
SyncTaskExecutor
:此实现不会异步运行调用。相反,每次调用都在调用线程中进行。它主要用于不需要多线程的情况,比如简单的测试用例。SimpleAsyncTaskExecutor
:该实现不会重用任何线程。相反,它为每次调用启动一个新线程。不过,它支持并发限制,当超过限制时,会阻塞调用,直到有空闲槽位。如果你需要真正的线程池,请查看本列表后面的ThreadPoolTaskExecutor
。当启用 "virtualThreads" 选项时,它将使用 JDK 21 的虚拟线程。这个实现还通过 Spring 的生命周期管理支持优雅关闭。ConcurrentTaskExecutor
:此实现是java.util.concurrent.Executor
实例的适配器。还有一个替代方案 (ThreadPoolTaskExecutor
),它将Executor
配置参数作为 Bean 属性暴露出来。很少需要直接使用ConcurrentTaskExecutor
。但是,如果ThreadPoolTaskExecutor
不够灵活以满足你的需求,ConcurrentTaskExecutor
是一个替代选择。ThreadPoolTaskExecutor
:这个实现是最常用的。它暴露了用于配置java.util.concurrent.ThreadPoolExecutor
的 Bean 属性,并将其包装在TaskExecutor
中。如果你需要适配不同类型的java.util.concurrent.Executor
,我们建议你使用ConcurrentTaskExecutor
。它还通过 Spring 的生命周期管理提供了暂停/恢复功能和优雅关闭功能。DefaultManagedTaskExecutor
:此实现使用在 JSR - 236 兼容的运行时环境(如 Jakarta EE 应用服务器)中通过 JNDI 获取的ManagedExecutorService
,用于替代 CommonJ WorkManager。
# 1.2、使用 TaskExecutor
Spring 的 TaskExecutor
实现通常与依赖注入一起使用。在下面的示例中,我们定义了一个 Bean,它使用 ThreadPoolTaskExecutor
异步打印一组消息:
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
ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(25);
return taskExecutor;
}
@Bean
TaskExecutorExample taskExecutorExample(ThreadPoolTaskExecutor taskExecutor) {
return new TaskExecutorExample(taskExecutor);
}
<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>
大多数 TaskExecutor
实现都提供了一种方法,可以用 TaskDecorator
自动包装提交的任务。装饰器应该委托给它所包装的任务,并且可能在任务执行前后实现自定义行为。
让我们考虑一个简单的实现,它将在任务执行前后记录消息:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.task.TaskDecorator;
public class LoggingTaskDecorator implements TaskDecorator {
private static final Log logger = LogFactory.getLog(LoggingTaskDecorator.class);
@Override
public Runnable decorate(Runnable runnable) {
return () -> {
logger.debug("Before execution of " + runnable);
runnable.run();
logger.debug("After execution of " + runnable);
};
}
}
然后我们可以在 TaskExecutor
实例上配置我们的装饰器:
@Bean
ThreadPoolTaskExecutor decoratedTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setTaskDecorator(new LoggingTaskDecorator());
return taskExecutor;
}
<bean id="decoratedTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="taskDecorator" ref="loggingTaskDecorator"/>
</bean>
如果需要多个装饰器,可以使用 org.springframework.core.task.support.CompositeTaskDecorator
来顺序执行多个装饰器。
# 2、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);
}
最简单的方法是只接受一个 Runnable
和一个 Instant
的 schedule
方法。这会使任务在指定时间之后运行一次。其他所有方法都能够安排任务重复运行。固定速率和固定延迟方法用于简单的周期性执行,但接受 Trigger
的方法更加灵活。
# 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();
}
# 2.2、Trigger
实现
Spring 提供了 Trigger
接口的两种实现。最有趣的一种是 CronTrigger
。它允许根据 cron 表达式 来安排任务。例如,以下任务被安排在每个工作日的 9 点到 17 点之间,每隔 15 分钟运行一次:
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
另一种实现是 PeriodicTrigger
,它接受一个固定周期、一个可选的初始延迟值,以及一个布尔值,用于指示该周期应被解释为固定速率还是固定延迟。由于 TaskScheduler
接口已经定义了以固定速率或固定延迟安排任务的方法,因此应尽可能直接使用这些方法。PeriodicTrigger
实现的价值在于,你可以在依赖 Trigger
抽象的组件中使用它。例如,允许周期性触发器、基于 cron 的触发器,甚至自定义触发器实现可以互换使用可能会很方便。这样的组件可以利用依赖注入,以便你可以从外部配置这些 Trigger
,从而轻松修改或扩展它们。
# 2.3、TaskScheduler
实现
与 Spring 的 TaskExecutor
抽象一样,TaskScheduler
架构的主要好处是,应用程序的调度需求与部署环境解耦。这种抽象级别在部署到应用服务器环境时尤为重要,在这种环境中,应用程序本身不应直接创建线程。对于这种情况,Spring 提供了 DefaultManagedTaskScheduler
,它在 Jakarta EE 环境中委托给 JSR - 236 的 ManagedScheduledExecutorService
。
每当不需要外部线程管理时,一个更简单的替代方案是在应用程序内部设置一个本地 ScheduledExecutorService
,可以通过 Spring 的 ConcurrentTaskScheduler
进行适配。为了方便起见,Spring 还提供了 ThreadPoolTaskScheduler
,它在内部委托给 ScheduledExecutorService
,以提供类似于 ThreadPoolTaskExecutor
的通用 Bean 风格配置。这些变体在宽松的应用服务器环境(特别是在 Tomcat 和 Jetty 上)中的本地嵌入式线程池设置中也能完美工作。
从 6.1 版本开始,ThreadPoolTaskScheduler
通过 Spring 的生命周期管理提供了暂停/恢复功能和优雅关闭功能。还有一个名为 SimpleAsyncTaskScheduler
的新选项,它与 JDK 21 的虚拟线程对齐,使用单个调度器线程,但为每个定时任务执行启动一个新线程(除了固定延迟任务,所有固定延迟任务都在单个调度器线程上运行,因此对于这个与虚拟线程对齐的选项,建议使用固定速率和 cron 触发器)。
# 3、调度和异步执行的注解支持
Spring 为任务调度和异步方法执行提供了注解支持。
# 3.1、启用调度注解
要启用对 @Scheduled
和 @Async
注解的支持,你可以将 @EnableScheduling
和 @EnableAsync
添加到你的一个 @Configuration
类中,或者使用 <task:annotation-driven>
元素,如下例所示:
@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfiguration {
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task
https://www.springframework.org/schema/task/spring-task.xsd">
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
</beans>
你可以为你的应用程序选择相关的注解。例如,如果你只需要对 @Scheduled
的支持,你可以省略 @EnableAsync
。为了进行更细粒度的控制,你还可以实现 SchedulingConfigurer
接口、AsyncConfigurer
接口,或者两者都实现。有关详细信息,请参阅 SchedulingConfigurer (opens new window) 和 AsyncConfigurer (opens new window) 的 Java 文档。
请注意,使用上述 XML 时,会提供一个执行器引用,用于处理与带有 @Async
注解的方法对应的任务,同时提供一个调度器引用,用于管理带有 @Scheduled
注解的方法。
注意:处理 @Async
注解的默认通知模式是 proxy
,这只允许通过代理拦截调用。同一类中的本地调用无法以这种方式被拦截。对于更高级的拦截模式,可以考虑结合编译时或加载时织入切换到 aspectj
模式。
# 3.2、@Scheduled
注解
你可以将 @Scheduled
注解添加到方法上,并附带触发元数据。例如,以下方法以固定延迟每五秒(5000 毫秒)调用一次,这意味着周期是从每次前一次调用的完成时间开始计算的:
@Scheduled(fixedDelay = 5000)
public void doSomething() {
// 应该定期运行的操作
}
注意:默认情况下,固定延迟、固定速率和初始延迟值将使用毫秒作为时间单位。如果你想使用其他时间单位,如秒或分钟,你可以通过 @Scheduled
中的 timeUnit
属性进行配置。
例如,上述示例也可以写成如下形式:
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
// 应该定期运行的操作
}
如果你需要固定速率执行,你可以在注解中使用 fixedRate
属性。以下方法每隔五秒(从每次调用的连续开始时间计算)调用一次:
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
// 应该定期运行的操作
}
对于固定延迟和固定速率任务,你可以通过指示在方法首次执行之前等待的时间量来指定初始延迟,如下 fixedRate
示例所示:
@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void doSomething() {
// 应该定期运行的操作
}
对于一次性任务,你可以通过指示在方法预期执行之前等待的时间量来指定初始延迟:
@Scheduled(initialDelay = 1000)
public void doSomething() {
// 只应运行一次的操作
}
如果简单的周期性执行不够灵活,你可以提供一个 cron 表达式。以下示例仅在工作日运行:
@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
// 只应在工作日运行的操作
}
提示:你还可以使用 zone
属性指定解析 cron 表达式时所使用的时区。
请注意,要调度的方法必须返回 void
且不能接受任何参数。如果方法需要与应用程序上下文中的其他对象进行交互,这些对象通常应通过依赖注入提供。
@Scheduled
可以用作可重复注解。如果在同一个方法上发现多个调度声明,每个声明将被独立处理,每个声明都有一个单独的触发器触发。因此,这些并置的调度可能会重叠,并并行或连续多次执行。请确保你指定的 cron 表达式等不会意外重叠。
注意:从 Spring Framework 4.3 开始,@Scheduled
方法支持任何作用域的 Bean。
请确保你不会在运行时初始化同一个 @Scheduled
注解类的多个实例,除非你确实希望为每个此类实例安排回调。与此相关,请确保你不会在使用 @Scheduled
注解并作为常规 Spring Bean 注册到容器中的 Bean 类上使用 @Configurable
。否则,你会得到双重初始化(一次通过容器,一次通过 @Configurable
切面),结果是每个 @Scheduled
方法会被调用两次。
# 3.3、响应式方法或 Kotlin 挂起函数上的 @Scheduled
注解
从 Spring Framework 6.1 开始,@Scheduled
方法也支持几种类型的响应式方法:
- 具有
Publisher
返回类型(或Publisher
的任何具体实现)的方法,如下例所示:
@Scheduled(fixedDelay = 500)
public Publisher<Void> reactiveSomething() {
// 返回一个 Publisher 实例
}
- 返回类型可以通过
ReactiveAdapterRegistry
的共享实例适配为Publisher
的方法,前提是该类型支持 延迟订阅,如下例所示:
@Scheduled(fixedDelay = 500)
public Single<String> rxjavaNonPublisher() {
return Single.just("example");
}
注意:CompletableFuture
类是一个通常可以适配为 Publisher
但不支持延迟订阅的类型示例。它在注册表中的 ReactiveAdapter
通过 getDescriptor().isDeferred()
方法返回 false
来表示这一点。
Kotlin 挂起函数,如下例所示:
@Scheduled(fixedDelay = 500) suspend fun something() { // 执行异步操作 }
返回 Kotlin
Flow
或Deferred
实例的方法,如下例所示:
@Scheduled(fixedDelay = 500)
fun something(): Flow<Void> {
flow {
// 执行异步操作
}
}
所有这些类型的方法都必须声明为不接受任何参数。对于 Kotlin 挂起函数,还必须存在 kotlinx.coroutines.reactor
桥接,以允许框架将挂起函数作为 Publisher
调用。
Spring Framework 会为带注解的方法获取一个 Publisher
实例,并安排一个 Runnable
,在其中订阅该 Publisher
。这些内部常规订阅根据相应的 cron
/fixedDelay
/fixedRate
配置进行。
如果 Publisher
发出 onNext
信号,这些信号将被忽略和丢弃(与同步 @Scheduled
方法的返回值被忽略的方式相同)。
在以下示例中,Flux
每五秒发出 onNext("Hello")
、onNext("World")
,但这些值未被使用:
@Scheduled(initialDelay = 5000, fixedRate = 5000)
public Flux<String> reactiveSomething() {
return Flux.just("Hello", "World");
}
如果 Publisher
发出 onError
信号,它将以 WARN
级别记录并恢复。由于 Publisher
实例的异步和惰性性质,异常不会从 Runnable
任务中抛出:这意味着 ErrorHandler
契约不适用于响应式方法。
结果是,尽管出现错误,后续的调度订阅仍会继续进行。
在以下示例中,Mono
订阅在最初的五秒内失败了两次。然后订阅开始成功,每五秒向标准输出打印一条消息:
@Scheduled(initialDelay = 0, fixedRate = 5000)
public Mono<Void> reactiveSomething() {
AtomicInteger countDown = new AtomicInteger(2);
return Mono.defer(() -> {
if (countDown.get() == 0 || countDown.decrementAndGet() == 0) {
return Mono.fromRunnable(() -> System.out.println("Message"));
}
return Mono.error(new IllegalStateException("Cannot deliver message"));
});
}
注意:在销毁带注解的 Bean 或关闭应用程序上下文时,Spring Framework 会取消调度任务,包括对 Publisher
的下一次调度订阅以及仍在运行的任何过去订阅(例如,对于长时间运行的发布者或甚至是无限发布者)。
# 3.4、@Async
注解
你可以在方法上添加 @Async
注解,使该方法的调用异步进行。换句话说,调用者在调用后立即返回,而方法的实际执行在提交给 Spring TaskExecutor
的任务中进行。在最简单的情况下,你可以将注解应用于返回 void
的方法,如下例所示:
@Async
void doSomething() {
// 这将异步运行
}
与带有 @Scheduled
注解的方法不同,这些方法可以接受参数,因为它们在运行时由调用者以“正常”方式调用,而不是由容器管理的调度任务调用。例如,以下代码是 @Async
注解的合法应用:
@Async
void doSomething(String s) {
// 这将异步运行
}
即使返回值的方法也可以异步调用。然而,此类方法必须具有 Future
类型的返回值。这仍然提供了异步执行的好处,使调用者可以在调用该 Future
的 get()
方法之前执行其他任务。以下示例展示了如何在返回值的方法上使用 @Async
:
@Async
Future<String> returnSomething(int i) {
// 这将异步运行
}
提示:@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
,并结合自定义切入点。
# 3.5、使用 @Async
进行执行器限定
默认情况下,当在方法上指定 @Async
时,使用的执行器是启用异步支持时配置的执行器,即如果你使用 XML,则是注解驱动元素;如果有,则是你的 AsyncConfigurer
实现。但是,当你需要指明在执行给定方法时应使用除默认执行器之外的执行器时,你可以使用 @Async
注解的 value
属性。以下示例展示了如何进行此操作:
@Async("otherExecutor")
void doSomething(String s) {
// 这将由 "otherExecutor" 异步运行
}
在这种情况下,"otherExecutor"
可以是 Spring 容器中任何 Executor
Bean 的名称,也可以是与任何 Executor
关联的限定符名称(例如,如使用 <qualifier>
元素或 Spring 的 @Qualifier
注解指定的)。
# 3.6、使用 @Async
进行异常管理
当 @Async
方法具有 Future
类型的返回值时,很容易管理方法执行期间抛出的异常,因为在调用 Future
结果的 get
方法时会抛出此异常。然而,对于返回 void
的方法,异常会被捕获不到且无法传播。你可以提供一个 AsyncUncaughtExceptionHandler
来处理此类异常。以下示例展示了如何进行此操作:
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// 处理异常
}
}
默认情况下,异常仅会被记录下来。你可以通过使用 AsyncConfigurer
或 <task:annotation-driven/>
XML 元素来定义自定义的 AsyncUncaughtExceptionHandler
。
# 4、task
命名空间
从 3.0 版本开始,Spring 包含了一个用于配置 TaskExecutor
和 TaskScheduler
实例的 XML 命名空间。它还提供了一种方便的方式来配置使用触发器调度的任务。
# 4.1、scheduler
元素
以下元素创建了一个具有指定线程池大小的 ThreadPoolTaskScheduler
实例:
<task:scheduler id="scheduler" pool-size="10"/>
为 id
属性提供的值用作线程池内线程名称的前缀。scheduler
元素相对简单。如果你不提供 pool-size
属性,默认线程池只有一个线程。调度器没有其他配置选项。
# 4.2、executor
元素
以下代码创建了一个 ThreadPoolTaskExecutor
实例:
<task:executor id="executor" pool-size="10"/>
与 上一节 中显示的调度器一样,为 id
属性提供的值用作线程池内线程名称的前缀。就线程池大小而言,executor
元素比 scheduler
元素支持更多的配置选项。一方面,ThreadPoolTaskExecutor
的线程池本身更可配置。执行器的线程池的核心大小和最大大小可以不同,而不仅仅是单个大小。如果你提供单个值,执行器将具有固定大小的线程池(核心大小和最大大小相同)。然而,executor
元素的 pool-size
属性也接受以 min-max
形式表示的范围。以下示例将最小值设置为 5
,最大值设置为 25
:
<task:executor
id="executorWithPoolSizeRange"
pool-size="5-25"
queue-capacity="100"/>
在上述配置中,还提供了一个 queue-capacity
值。线程池的配置还应考虑执行器的队列容量。有关线程池大小和队列容量之间关系的完整描述,请参阅 ThreadPoolExecutor (opens new window) 的文档。主要思想是,当提交一个任务时,如果活动线程的数量当前小于核心大小,执行器首先会尝试使用空闲线程。如果达到了核心大小,只要队列的容量尚未达到,任务就会被添加到队列中。只有在队列的容量达到之后,执行器才会在核心大小之外创建新线程。如果也达到了最大大小,执行器将拒绝该任务。
默认情况下,队列是无界的,但这很少是理想的配置,因为如果在所有线程池线程都忙碌时向该队列添加了足够多的任务,可能会导致 OutOfMemoryError
。此外,如果队列是无界的,最大大小根本不起作用。由于执行器在核心大小之外创建新线程之前总是会先尝试使用队列,因此队列必须有有限的容量,线程池才能超过核心大小增长(这就是为什么使用无界队列时,固定大小的线程池是唯一合理的情况)。
考虑上文提到的任务被拒绝的情况。默认情况下,当任务被拒绝时,线程池执行器会抛出 TaskRejectedException
。然而,拒绝策略实际上是可配置的。使用默认拒绝策略(即 AbortPolicy
实现)时会抛出异常。对于在高负载下可以跳过某些任务的应用程序,你可以改为配置 DiscardPolicy
或 DiscardOldestPolicy
。对于在高负载下需要限制提交任务的应用程序,另一个有效的选项是 CallerRunsPolicy
。该策略不是抛出异常或丢弃任务,而是强制调用提交方法的线程自己运行该任务。其思想是,这样的调用者在运行该任务时会忙碌,无法立即提交其他任务。因此,它提供了一种简单的方法来限制传入负载,同时保持线程池和队列的限制。通常,这允许执行器“赶上”它正在处理的任务,从而释放队列、线程池或两者的一些容量。你可以从 executor
元素的 rejection-policy
属性可用的枚举值中选择任何这些选项。
以下示例显示了一个 executor
元素,其中包含多个属性,用于指定各种行为:
<task:executor
id="executorWithCallerRunsPolicy"
pool-size="5-25"
queue-capacity="100"
rejection-policy="CALLER_RUNS"/>
最后,keep-alive
设置决定了线程在停止之前可以保持空闲的时间限制(以秒为单位)。如果当前线程池中的线程数量超过核心数量,在等待此时间而没有处理任务后,多余的线程将被停止。时间值为零会导致多余的线程在执行任务后立即停止,而不会在任务队列中保留后续工作。以下示例将 keep-alive
值设置为两分钟:
<task:executor
id="executorWithKeepAlive"
pool-size="5-25"
keep-alive="120"/>
# 4.3、scheduled-tasks
元素
Spring 任务命名空间最强大的功能是支持在 Spring 应用程序上下文中配置要调度的任务。这遵循了与 Spring 中其他“方法调用器”类似的方法,例如 JMS 命名空间为配置消息驱动 POJO 提供的方法。基本上,ref
属性可以指向任何 Spring 管理的对象,method
属性提供要在该对象上调用的方法的名称。以下是一个简单的示例:
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
调度器由外部元素引用,每个单独的任务都包含其触发元数据的配置。在上述示例中,该元数据定义了一个具有固定延迟的周期性触发器,表示每次任务执行完成后要等待的毫秒数。另一个选项是 fixed-rate
,表示方法应该运行的频率,而不管任何先前的执行需要多长时间。此外,对于 fixed-delay
和 fixed-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、表达式
所有 Spring cron 表达式都必须遵守相同的格式,无论你是在 @Scheduled 注解、task:scheduled-tasks 元素 中使用它们,还是在其他地方使用。一个格式良好的 cron 表达式,如 * * * * * *
,由六个用空格分隔的时间和日期字段组成,每个字段都有自己的有效取值范围:
┌───────────── 秒 (0 - 59)
│ ┌───────────── 分 (0 - 59)
│ │ ┌───────────── 小时 (0 - 23)
│ │ │ ┌───────────── 日期 (1 - 31)
│ │ │ │ ┌───────────── 月份 (1 - 12) 或 (JAN - DEC)
│ │ │ │ │ ┌───────────── 星期 (0 - 7)
│ │ │ │ │ │ (0 或 7 表示星期日,或 MON - SUN)
│ │ │ │ │ │
* * * * * *
有一些规则适用:
- 字段可以是星号 (
*
),它始终表示“从第一个到最后一个”。对于日期或星期字段,可以使用问号 (?
) 代替星号。 - 逗号 (
,
) 用于分隔列表中的项目。 - 用连字符 (
-
) 分隔的两个数字表示一个数字范围。指定的范围是包含的。 - 在范围(或
*
)后面跟/
表示通过该范围的数字值的间隔。 - 月份和星期字段也可以使用英文名称。使用特定日期或月份的前三个字母(不区分大小写)。
- 日期和星期字段可以包含
L
字符,它有不同的含义:- 在日期字段中,
L
表示该月的最后一天。如果后面跟负偏移量(即L - n
),则表示该月的倒数第n
天。 - 在星期字段中,
L
表示该周的最后一天。如果前面加数字或三个字母的名称(dL
或DDDL
),则表示该月中星期d
或DDD
的最后一天。
- 在日期字段中,
- 日期字段可以是
nW
,它表示该月第n
天最近的工作日。如果n
是星期六,则产生前一个星期五。如果n
是星期日,则产生后一个星期一;如果n
是1
且是星期六(即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 点和晚上 7 点 |
0 0/30 8 - 10 * * * | 每天的 8:00、8:30、9:00、9:30、10:00 和 10:30 |
0 0 9 - 17 * * MON - FRI | 工作日的 9 点到 17 点整 |
0 0 0 25 DEC ? | 每年圣诞节午夜 |
0 0 0 L * * | 每月最后一天的午夜 |
0 0 0 L - 3 * * | 每月倒数第三天的午夜 |
0 0 0 * * 5L | 每月最后一个星期五的午夜 |
0 0 0 * * THUL | 每月最后一个星期四的午夜 |
0 0 0 1W * * | 每月第一个工作日的午夜 |
0 0 0 LW * * | 每月最后一个工作日的午夜 |
0 0 0 ? * 5#2 | 每月第二个星期五的午夜 |
0 0 0 ? * MON#1 | 每月第一个星期一的午夜 |
# 5.1、宏
像 0 0 * * * *
这样的表达式很难让人解析,因此在出现错误时也很难修复。为了提高可读性,Spring 支持以下宏,它们代表常用的时间序列。你可以使用这些宏代替六位数的值,例如:@Scheduled(cron = "@hourly")
。
宏 | 含义 |
---|---|
@yearly (或 @annually ) | 每年一次 (0 0 0 1 1 * ) |
@monthly | 每月一次 (0 0 0 1 * * ) |
@weekly | 每周一次 (0 0 0 * * 0 ) |
@daily (或 @midnight ) | 每天一次 (0 0 0 * * * ) |
@hourly | 每小时一次 (0 0 * * * * ) |
# 6、使用 Quartz 调度器
Quartz 使用 Trigger
、Job
和 JobDetail
对象来实现各种作业的调度。有关 Quartz 背后的基本概念,请参阅 Quartz 网站 (opens new window)。为了方便起见,Spring 提供了几个类,可以简化在基于 Spring 的应用程序中使用 Quartz。
# 6.1、使用 JobDetailFactoryBean
Quartz JobDetail
对象包含运行作业所需的所有信息。Spring 提供了一个 JobDetailFactoryBean
,它支持使用 Bean 风格的属性进行 XML 配置。考虑以下示例:
<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="example.ExampleJob"/>
<property name="jobDataAsMap">
<map>
<entry key="timeout" value="5"/>
</map>
</property>
</bean>
作业详情配置包含了运行作业(ExampleJob
)所需的所有信息。超时时间在作业数据映射中指定。作业数据映射可通过 JobExecutionContext
(在执行时传递给你)获取,但 JobDetail
也会将作业数据映射中的属性应用到作业实例的属性上。因此,在以下示例中,ExampleJob
包含一个名为 timeout
的 Bean 属性,并且 JobDetail
会自动将其应用:
package example;
public class ExampleJob extends QuartzJobBean {
private int timeout;
/**
* 在 ExampleJob 实例化后,使用 JobDetailFactoryBean 中的值调用的 setter 方法
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
// 执行实际工作
}
}
作业数据映射中的所有其他附加属性也可供你使用。
注意:通过使用 name
和 group
属性,你可以分别修改作业的名称和组。默认情况下,作业的名称与 JobDetailFactoryBean
的 Bean 名称匹配(在上述示例中为 exampleJob
)。
# 6.2、使用 MethodInvokingJobDetailFactoryBean
通常,你只需要在特定对象上调用一个方法。通过使用 MethodInvokingJobDetailFactoryBean
,你可以实现这一点,如下例所示:
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
</bean>
上述示例将在 exampleBusinessObject
方法上调用 doIt
方法,如下例所示:
public class ExampleBusinessObject {
// 属性和协作对象
public void doIt() {
// 执行实际工作
}
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>
通过使用 MethodInvokingJobDetailFactoryBean
,你无需创建仅用于调用方法的单行作业。你只需创建实际的业务对象并连接详情对象即可。
默认情况下,Quartz 作业是无状态的,这可能导致作业相互干扰。如果你为同一个 JobDetail
指定了两个触发器,则可能在第一个作业完成之前就启动第二个作业。如果 JobDetail
类实现了 Stateful
接口,则不会出现这种情况:第二个作业会在第一个作业完成后才启动。
要使 MethodInvokingJobDetailFactoryBean
生成的作业是非并发的,请将 concurrent
标志设置为 false
,如下例所示:
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
<property name="concurrent" value="false"/>
</bean>
注意:默认情况下,作业将以并发方式运行。
# 6.3、使用触发器和 SchedulerFactoryBean
编排作业
我们已经创建了作业详情和作业。我们还回顾了允许在特定对象上调用方法的便捷 Bean。当然,我们仍然需要对作业本身进行调度。这可以通过使用触发器和 SchedulerFactoryBean
来完成。Quartz 中提供了几种触发器,Spring 提供了两种具有便捷默认值的 Quartz FactoryBean
实现:CronTriggerFactoryBean
和 SimpleTriggerFactoryBean
。
触发器需要进行调度。Spring 提供了一个 SchedulerFactoryBean
,它将触发器作为属性进行设置。SchedulerFactoryBean
使用这些触发器对实际的作业进行调度。
以下示例同时使用了 SimpleTriggerFactoryBean
和 CronTriggerFactoryBean
:
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<!-- 请参阅上面方法调用作业的示例 -->
<property name="jobDetail" ref="jobDetail"/>
<!-- 10 秒 -->
<property name="startDelay" value="10000"/>
<!-- 每 50 秒重复一次 -->
<property name="repeatInterval" value="50000"/>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="exampleJob"/>
<!-- 每天早上 6 点运行 -->
<property name="cronExpression" value="0 0 6 * * ?"/>
</bean>
上述示例设置了两个触发器,一个在延迟 10 秒后每 50 秒运行一次,另一个每天早上 6 点运行。为了完成所有设置,我们需要设置 SchedulerFactoryBean
,如下例所示:
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
<ref bean="simpleTrigger"/>
</list>
</property>
</bean>
SchedulerFactoryBean
还有更多属性可用,例如作业详情使用的日历、用于自定义 Quartz 的属性以及 Spring 提供的 JDBC 数据源。有关更多信息,请参阅 SchedulerFactoryBean (opens new window) 的 Java 文档。
注意:SchedulerFactoryBean
还会识别类路径中的 quartz.properties
文件,该文件基于 Quartz 属性键,与常规 Quartz 配置相同。请注意,许多 SchedulerFactoryBean
设置会与属性文件中的常见 Quartz 设置相互作用;因此,不建议在两个级别都指定值。例如,如果你打算依赖 Spring 提供的数据源,则不要设置 "org.quartz.jobStore.class" 属性,或者指定 org.springframework.scheduling.quartz.LocalDataSourceJobStore
变体,它是标准 org.quartz.impl.jdbcjobstore.JobStoreTX
的完整替代品。
# 六、缓存抽象
从3.1版本开始,Spring框架支持为现有的Spring应用程序透明地添加缓存功能。与事务)支持类似,缓存抽象允许一致地使用各种缓存解决方案,同时对代码的影响最小化。
在Spring框架4.1中,缓存抽象得到了显著扩展,增加了对JSR - 107 注解)的支持以及更多的自定义选项。
# 1、章节概要
- 理解缓存抽象)
- 基于声明式注解的缓存)
- JCache(JSR - 107)注解)
- 基于声明式XML的缓存)
- 配置缓存存储)
- 接入不同的后端缓存)
- 如何设置TTL/TTI/清除策略/其他功能?)
# 2、理解缓存抽象
# 2.1、缓存与缓冲区的区别
“缓冲区”和“缓存”这两个术语常常被互换使用。不过要注意,它们代表着不同的事物。传统上,缓冲区用作快速实体和慢速实体之间数据的中间临时存储。由于一方必须等待另一方(这会影响性能),缓冲区通过允许整块数据一次性移动而非小块移动来缓解这个问题。数据从缓冲区只被写入和读取一次。此外,缓冲区至少对一方是可见的,该方知道它的存在。
而缓存,根据定义,是隐藏的,双方都不知道缓存操作的发生。它也能提高性能,不过是通过让同一数据能够被快速多次读取来实现的。
你可以点击此处 (opens new window)进一步了解缓冲区和缓存之间的区别。
核心来说,缓存抽象将缓存应用于Java方法,从而根据缓存中可用的信息减少方法的执行次数。也就是说,每次调用目标方法时,该抽象会应用一种缓存行为,检查该方法是否已经为给定的参数调用过。如果已经调用过,就返回缓存的结果,而无需实际调用该方法。如果该方法尚未调用过,则调用该方法,将结果缓存起来并返回给用户,这样,下次调用该方法时,就返回缓存的结果。通过这种方式,对于给定的一组参数,昂贵的方法(无论是CPU密集型还是IO密集型)只需调用一次,且无需再次实际调用该方法,就可以复用结果。缓存逻辑会透明地应用,不会对调用者产生任何干扰。
重要提示:这种方法仅适用于那些无论调用多少次,对于给定输入(或参数)都能保证返回相同输出(结果)的方法。
缓存抽象还提供了其他与缓存相关的操作,例如更新缓存内容或移除一个或所有条目。如果缓存处理的是在应用程序运行过程中可能发生变化的数据,这些操作会很有用。
与Spring框架中的其他服务一样,缓存服务是一种抽象(而非具体的缓存实现),需要使用实际的存储来存储缓存数据 —— 也就是说,该抽象使你不必编写缓存逻辑,但不提供实际的数据存储。这个抽象由org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口实现。
Spring为该抽象提供了几种实现方式:基于JDK的java.util.concurrent.ConcurrentMap
的缓存、Gemfire缓存、Caffeine (opens new window)以及符合JSR - 107标准的缓存(如Ehcache 3.x)。有关集成其他缓存存储和提供者的更多信息,请参阅集成不同的后端缓存。
重要提示:缓存抽象没有针对多线程和多进程环境进行特殊处理,这些特性由缓存实现来处理。
如果你有一个多进程环境(即应用程序部署在多个节点上),你需要相应地配置缓存提供者。根据具体用例,在多个节点上复制相同的数据可能就足够了。但是,如果在应用程序运行过程中要更改数据,你可能需要启用其他传播机制。
对特定项进行缓存,相当于在以编程方式与缓存交互时常见的“先获取,如果未找到则继续执行,最后存入”的代码块。这里不应用锁,多个线程可能会同时尝试加载同一项目。这同样适用于缓存项的淘汰操作。如果多个线程同时尝试更新或淘汰数据,你可能会使用到陈旧的数据。某些缓存提供者在此方面提供了高级功能。更多细节请参阅你的缓存提供者的文档。
要使用缓存抽象,需要考虑两个方面:
- 缓存声明:确定需要缓存的方法及其策略。
- 缓存配置:数据存储和读取所依赖的底层缓存。
# 3、基于声明式注解的缓存
对于缓存声明,Spring 的缓存抽象提供了一组 Java 注解:
@Cacheable
:触发缓存填充。@CacheEvict
:触发缓存清除。@CachePut
:更新缓存,且不影响方法的执行。@Caching
:将多个缓存操作组合在一个方法上。@CacheConfig
:在类级别共享一些常见的缓存相关设置。
# 3.1、@Cacheable
注解
顾名思义,可以使用 @Cacheable
来标记可缓存的方法,即方法的执行结果会存储在缓存中。这样,后续使用相同参数调用该方法时,会直接从缓存中返回值,而无需实际调用该方法。最简单的使用形式中,注解声明需要指定与被注解方法关联的缓存名称,如下例所示:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在上述代码片段中,findBook
方法与名为 books
的缓存相关联。每次调用该方法时,会检查缓存中是否已经执行过该调用,是否不需要再次执行。虽然在大多数情况下,只声明一个缓存,但该注解允许指定多个名称,以便使用多个缓存。此时,在调用方法之前会检查每个缓存,如果至少有一个缓存命中,则返回关联的值。
注意:所有不包含该值的其他缓存也会被更新,即使实际上并未调用被缓存的方法。
以下示例在 findBook
方法上使用 @Cacheable
并指定了多个缓存:
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
# a、默认键生成
由于缓存本质上是键值存储,每个缓存方法的调用都需要转换为适合缓存访问的键。缓存抽象使用一个简单的 KeyGenerator
,其算法如下:
- 如果没有提供参数,则返回
SimpleKey.EMPTY
。 - 如果只提供了一个参数,则返回该实例。
- 如果提供了多个参数,则返回一个包含所有参数的
SimpleKey
。
只要参数有自然键并实现了有效的 hashCode()
和 equals()
方法,这种方法适用于大多数用例。如果情况并非如此,则需要更改策略。
若要提供不同的默认键生成器,需要实现 org.springframework.cache.interceptor.KeyGenerator
接口。
注意:Spring 4.0 发布后,默认的键生成策略发生了变化。早期版本的 Spring 使用的键生成策略在处理多个键参数时,只考虑参数的 hashCode()
而不考虑 equals()
,这可能会导致意外的键冲突(详情见 spring - framework#14870 (opens new window))。新的 SimpleKeyGenerator
在此类场景中使用复合键。
如果想继续使用以前的键策略,可以配置已弃用的 org.springframework.cache.interceptor.DefaultKeyGenerator
类,或创建一个基于自定义哈希的 KeyGenerator
实现。
# b、自定义键生成声明
由于缓存是通用的,目标方法很可能有各种不同的签名,无法直接映射到缓存结构上。当目标方法有多个参数,而其中只有部分参数适合用于缓存(其余参数仅用于方法逻辑)时,这个问题就显得尤为突出。考虑以下示例:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然这两个 boolean
类型的参数会影响查找书籍的方式,但它们对缓存并无用处。此外,如果两个参数中只有一个重要,而另一个不重要,该怎么办呢?
对于这种情况,@Cacheable
注解允许通过其 key
属性指定如何生成键。可以使用 SpEL 来选择感兴趣的参数(或其嵌套属性)、执行操作,甚至调用任意方法,而无需编写任何代码或实现任何接口。这是比 默认生成器 更推荐的方法,因为随着代码库的增长,方法的签名往往差异很大。默认策略可能适用于某些方法,但很少适用于所有方法。
以下示例展示了各种 SpEL 声明(如果不熟悉 SpEL,建议阅读 Spring 表达式语言):
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
上述代码片段展示了选择特定参数、其属性之一,甚至调用任意(静态)方法是多么容易。
如果负责生成键的算法过于特殊,或者需要共享,可以在操作中定义一个自定义的 keyGenerator
。为此,需要指定要使用的 KeyGenerator
bean 实现的名称,如下例所示:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
注意:key
和 keyGenerator
参数是互斥的,若一个操作同时指定了这两个参数,将抛出异常。
# c、默认缓存解析
缓存抽象使用一个简单的 CacheResolver
,它通过配置的 CacheManager
检索在操作级别定义的缓存。
若要提供不同的默认缓存解析器,需要实现 org.springframework.cache.interceptor.CacheResolver
接口。
# d、自定义缓存解析
默认的缓存解析适用于使用单个 CacheManager
且没有复杂缓存解析需求的应用程序。
对于使用多个缓存管理器的应用程序,可以为每个操作设置要使用的 cacheManager
,如下例所示:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") // (1)
public Book findBook(ISBN isbn) {...}
- 指定
anotherCacheManager
。
也可以像替换 键生成 那样完全替换 CacheResolver
。每个缓存操作都会请求进行解析,这样实现就可以根据运行时参数实际解析要使用的缓存。以下示例展示了如何指定 CacheResolver
:
@Cacheable(cacheResolver="runtimeCacheResolver") // (1)
public Book findBook(ISBN isbn) {...}
- 指定
CacheResolver
。
注意:从 Spring 4.1 开始,缓存注解的 value
属性不再是必需的,因为 CacheResolver
可以提供此特定信息,而无需考虑注解的内容。
与 key
和 keyGenerator
类似,cacheManager
和 cacheResolver
参数是互斥的,若一个操作同时指定了这两个参数,将抛出异常,因为 CacheResolver
实现会忽略自定义的 CacheManager
,这可能并非你所期望的。
# e、同步缓存
在多线程环境中,某些操作可能会针对相同参数并发调用(通常在启动时)。默认情况下,缓存抽象不会加锁,同一个值可能会被多次计算,这就失去了缓存的意义。
对于这些特定情况,可以使用 sync
属性来指示底层缓存提供程序在计算值时锁定缓存条目。这样,只有一个线程会忙于计算值,其他线程会被阻塞,直到该条目在缓存中更新。以下示例展示了如何使用 sync
属性:
@Cacheable(cacheNames="foos", sync=true) // (1)
public Foo executeExpensiveOperation(String id) {...}
- 使用
sync
属性。
注意:这是一个可选功能,你常用的缓存库可能不支持。核心框架提供的所有 CacheManager
实现都支持此功能。更多详细信息,请参阅缓存提供程序的文档。
# f、使用 CompletableFuture 和响应式返回类型进行缓存
从 6.1 版本开始,缓存注解会考虑 CompletableFuture
和响应式返回类型,并相应地自动调整缓存交互。
对于返回 CompletableFuture
的方法,该 future 产生的对象在完成时会被缓存,并且缓存命中的查找将通过 CompletableFuture
进行:
@Cacheable("books")
public CompletableFuture<Book> findBook(ISBN isbn) {...}
对于返回 Reactor Mono
的方法,该响应式流发布者发出的对象在可用时会被缓存,并且缓存命中的查找将作为 Mono
(由 CompletableFuture
支持)进行检索:
@Cacheable("books")
public Mono<Book> findBook(ISBN isbn) {...}
对于返回 Reactor Flux
的方法,该响应式流发布者发出的对象会在完成时收集到一个 List
中并缓存,并且缓存命中的查找将作为 Flux
(由缓存的 List
值的 CompletableFuture
支持)进行检索:
@Cacheable("books")
public Flux<Book> findBooks(String author) {...}
这种 CompletableFuture
和响应式适配同样适用于同步缓存,在发生并发缓存未命中的情况下,值只会计算一次:
@Cacheable(cacheNames="foos", sync=true) // (1)
public CompletableFuture<Foo> executeExpensiveOperation(String id) {...}
- 使用
sync
属性。
注意:为了使这种设置在运行时生效,配置的缓存需要能够基于 CompletableFuture
进行检索。Spring 提供的 ConcurrentMapCacheManager
会自动适应这种检索方式,而 CaffeineCacheManager
在启用异步缓存模式时原生支持该功能:在 CaffeineCacheManager
实例上设置 setAsyncCacheMode(true)
。
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheSpecification(...);
cacheManager.setAsyncCacheMode(true);
return cacheManager;
}
最后需要注意的是,基于注解的缓存不适用于涉及组合和背压的复杂响应式交互。如果选择在特定的响应式方法上声明 @Cacheable
,请考虑这种相当粗粒度的缓存交互的影响,因为它只是简单地存储 Mono
发出的对象,或者 Flux
的预收集对象列表。
# g、条件缓存
有时,一个方法可能并非一直都适合缓存(例如,可能取决于给定的参数)。缓存注解通过 condition
参数支持此类用例,该参数接受一个 SpEL
表达式,其计算结果为 true
或 false
。如果为 true
,则对该方法进行缓存;否则,该方法的行为就像没有被缓存一样(即无论缓存中存储了什么值或使用了什么参数,每次都会调用该方法)。例如,以下方法仅在参数 name
的长度小于 32 时才会被缓存:
@Cacheable(cacheNames="book", condition="#name.length() < 32") // (1)
public Book findBook(String name)
- 在
@Cacheable
上设置条件。
除了 condition
参数,还可以使用 unless
参数来阻止将值添加到缓存中。与 condition
不同,unless
表达式在方法调用之后进行计算。扩展前面的示例,也许我们只想缓存平装书,如下所示:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") // (1)
public Book findBook(String name)
- 使用
unless
属性来排除精装书。
缓存抽象支持 java.util.Optional
返回类型。如果 Optional
值存在,则将其存储在关联的缓存中;如果 Optional
值不存在,则将 null
存储在关联的缓存中。#result
始终引用业务实体,而不是支持的包装器,因此前面的示例可以重写如下:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
注意,#result
仍然引用 Book
,而不是 Optional<Book>
。由于它可能为 null
,因此我们使用 SpEL 的 安全导航运算符。
# h、可用的缓存 SpEL 评估上下文
每个 SpEL
表达式都针对一个专用的 上下文 进行评估。除了内置参数外,框架还提供了专用的与缓存相关的元数据,例如参数名称。下表描述了提供给上下文的项,以便可以使用它们进行键和条件计算:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | 根对象 | 被调用方法的名称 | #root.methodName |
method | 根对象 | 被调用的方法 | #root.method.name |
target | 根对象 | 被调用的目标对象 | #root.target |
targetClass | 根对象 | 被调用目标的类 | #root.targetClass |
args | 根对象 | 用于调用目标的参数(作为对象数组) | #root.args[0] |
caches | 根对象 | 当前方法所使用的缓存集合 | #root.caches[0].name |
参数名称 | 评估上下文 | 特定方法参数的名称。如果名称不可用(例如,因为代码是在没有 -parameters 标志的情况下编译的),也可以使用 #a<#arg> 语法访问各个参数,其中 <#arg> 表示参数索引(从 0 开始) | #iban 或 #a0 (也可以使用 #p0 或 #p<#arg> 表示法作为别名) |
result | 评估上下文 | 方法调用的结果(即要缓存的值)。仅在 unless 表达式、缓存放入 表达式(用于计算 key )或 缓存清除 表达式(当 beforeInvocation 为 false 时)中可用。对于支持的包装器(如 Optional ),#result 引用实际对象,而不是包装器 | #result |
# 3.2、@CachePut
注解
当需要在不影响方法执行的情况下更新缓存时,可以使用 @CachePut
注解。也就是说,该方法总是会被调用,并且其结果会被放入缓存中(根据 @CachePut
的选项)。它支持与 @Cacheable
相同的选项,应该用于缓存填充,而不是方法流程优化。以下示例使用了 @CachePut
注解:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
重要:通常强烈不建议在同一个方法上同时使用 @CachePut
和 @Cacheable
注解,因为它们的行为不同。@Cacheable
会通过使用缓存来跳过方法调用,而 @CachePut
会强制调用方法以进行缓存更新,这会导致意外的行为。除了特定的边缘情况(例如,注解的条件相互排斥)外,应避免这样的声明。此外,这些条件不应依赖于结果对象(即 #result
变量),因为它们需要提前验证以确保相互排斥。
从 6.1 版本开始,@CachePut
会考虑 CompletableFuture
和响应式返回类型,并在产生的对象可用时执行放入操作。
# 3.3、@CacheEvict
注解
缓存抽象不仅允许填充缓存存储,还允许清除缓存。这个过程对于从缓存中移除陈旧或未使用的数据很有用。与 @Cacheable
相反,@CacheEvict
用于标记执行缓存清除的方法(即作为从缓存中移除数据的触发方法)。与 @Cacheable
类似,@CacheEvict
要求指定受该操作影响的一个或多个缓存,允许指定自定义的缓存和键解析方式或条件,并且有一个额外的参数(allEntries
),用于指示是需要执行整个缓存区域的清除,而不仅仅是清除一个条目(基于键)。以下示例清除 books
缓存中的所有条目:
@CacheEvict(cacheNames="books", allEntries=true) // (1)
public void loadBooks(InputStream batch)
- 使用
allEntries
属性清除缓存中的所有条目。
当需要清除整个缓存区域时,这个选项非常有用。与逐个清除每个条目(效率低下,且会花费很长时间)不同,前面的示例通过一个操作移除了所有条目。请注意,在这种情况下,框架会忽略指定的任何键,因为它不适用(整个缓存被清除,而不仅仅是一个条目)。
还可以使用 beforeInvocation
属性来指示清除操作是在方法调用之后(默认)还是之前发生。前者提供了与其他注解相同的语义:一旦方法成功完成,就会对缓存执行操作(在这种情况下是清除操作)。如果方法没有运行(因为可能被缓存)或抛出异常,则不会执行清除操作。而 beforeInvocation = true
会导致清除操作始终在方法调用之前发生,这在清除操作不需要依赖方法执行结果的情况下很有用。
需要注意的是,void
方法可以与 @CacheEvict
一起使用,因为这些方法只是作为触发操作,返回值会被忽略(因为它们不与缓存交互),而 @Cacheable
需要返回值,因为它会向缓存中添加数据或更新缓存中的数据。
从 6.1 版本开始,@CacheEvict
会考虑 CompletableFuture
和响应式返回类型,并在处理完成后执行调用后清除操作。
# 3.4、@Caching
注解
有时,需要指定多个相同类型的注解(如 @CacheEvict
或 @CachePut
),例如,因为不同缓存之间的条件或键表达式不同。@Caching
允许在同一个方法上使用多个嵌套的 @Cacheable
、@CachePut
和 @CacheEvict
注解。以下示例使用了两个 @CacheEvict
注解:
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
# 3.5、@CacheConfig
注解
到目前为止,我们已经看到缓存操作提供了许多自定义选项,并且可以为每个操作设置这些选项。然而,如果某些自定义选项适用于类的所有操作,配置它们可能会变得很繁琐。例如,可以使用单个类级别定义来替代为类的每个缓存操作指定要使用的缓存名称,这就是 @CacheConfig
发挥作用的地方。以下示例使用 @CacheConfig
来设置缓存名称:
@CacheConfig("books") // (1)
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}
- 使用
@CacheConfig
来设置缓存名称。
@CacheConfig
是一个类级别的注解,它允许共享缓存名称、自定义 KeyGenerator
、自定义 CacheManager
和自定义 CacheResolver
。在类上放置此注解不会开启任何缓存操作。
操作级别的自定义设置总是会覆盖 @CacheConfig
上设置的自定义选项。因此,每个缓存操作有三个级别的自定义设置:
- 全局配置,例如,通过
CachingConfigurer
进行配置,请参阅下一节。 - 类级别,使用
@CacheConfig
。 - 操作级别。
注意:特定于提供者的设置通常可在 CacheManager
bean 上使用,例如,在 CaffeineCacheManager
上。这些设置实际上也是全局的。
# 3.6、启用缓存注解
需要注意的是,即使声明了缓存注解,这些注解本身并不会自动触发其相应的操作。与 Spring 中的许多功能一样,需要声明式地启用该功能(这意味着如果觉得缓存可能是问题所在,只需移除一行配置,而不是删除代码中的所有注解,就可以禁用缓存功能)。
要启用缓存注解,可以在一个 @Configuration
类上添加 @EnableCaching
注解,或者在 XML 中使用 cache:annotation - driven
元素:
@Configuration
@EnableCaching
class CacheConfiguration {
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheSpecification("...");
return cacheManager;
}
}
<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/>
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
<property name="cacheSpecification" value="..."/>
</bean>
</beans>
cache:annotation - driven
元素和 @EnableCaching
注解都允许指定各种选项,这些选项会影响通过 AOP 将缓存行为添加到应用程序中的方式。该配置与 @Transactional 的配置有意保持相似。
注意:处理缓存注解的默认通知模式是 proxy
,这种模式只允许通过代理拦截方法调用。同一类中的本地调用无法以这种方式被拦截。如果需要更高级的拦截模式,可以考虑结合编译时或加载时织入,将模式切换为 aspectj
。
注意:有关实现 CachingConfigurer
所需的高级自定义(使用 Java 配置)的更多详细信息,请参阅 javadoc (opens new window)。
XML 属性 | 注解属性 | 默认值 | 描述 |
---|---|---|---|
cache - manager | N/A (请参阅 CachingConfigurer (opens new window) 的 javadoc) | cacheManager | 要使用的缓存管理器的名称。幕后会使用此缓存管理器(如果未设置,则使用 cacheManager )初始化一个默认的 CacheResolver 。若要对缓存解析进行更细粒度的管理,可以考虑设置 cache - resolver 属性。 |
cache - resolver | N/A (请参阅 CachingConfigurer (opens new window) 的 javadoc) | 使用配置的 cacheManager 的 SimpleCacheResolver | 用于解析后备缓存的 CacheResolver 的 bean 名称。此属性不是必需的,仅作为 cache - manager 属性的替代项时才需指定。 |
key - generator | N/A (请参阅 CachingConfigurer (opens new window) 的 javadoc) | SimpleKeyGenerator | 要使用的自定义键生成器的名称。 |
error - handler | N/A (请参阅 CachingConfigurer (opens new window) 的 javadoc) | SimpleCacheErrorHandler | 要使用的自定义缓存错误处理程序的名称。默认情况下,缓存相关操作期间抛出的任何异常都会抛回给客户端。 |
mode | mode | proxy | 默认模式(proxy )使用 Spring 的 AOP 框架对带注解的 bean 进行代理(遵循前面讨论的代理语义,仅适用于通过代理进行的方法调用)。替代模式(aspectj )则使用 Spring 的 AspectJ 缓存切面编织受影响的类,修改目标类的字节码,以应用于任何类型的方法调用。AspectJ 编织需要在类路径中包含 spring - aspects.jar ,并且需要启用加载时编织(或编译时编织)。(有关如何设置加载时编织的详细信息,请参阅 Spring 配置。) |
proxy - target - class | proxyTargetClass | false | 仅适用于代理模式。控制为带有 @Cacheable 或 @CacheEvict 注解的类创建哪种类型的缓存代理。如果 proxy - target - class 属性设置为 true ,则创建基于类的代理。如果 proxy - target - class 为 false 或省略该属性,则创建标准的 JDK 接口-based 代理。(有关不同代理类型的详细检查,请参阅 代理机制。) |
order | order | Ordered.LOWEST_PRECEDENCE | 定义应用于带有 @Cacheable 或 @CacheEvict 注解的 bean 的缓存通知的顺序。(有关 AOP 通知排序规则的更多信息,请参阅 通知排序。)未指定排序意味着 AOP 子系统将确定通知的顺序。 |
注意:<cache:annotation - driven/>
仅在其定义的同一应用程序上下文中查找带有 @Cacheable/@CachePut/@CacheEvict/@Caching
注解的 bean。这意味着,如果将 <cache:annotation - driven/>
放在 DispatcherServlet
的 WebApplicationContext
中,它只会检查控制器中的 bean,而不会检查服务中的 bean。有关更多信息,请参阅 MVC 部分。
# 3.7、方法可见性和缓存注解
当使用代理时,应仅将缓存注解应用于具有公共可见性的方法。如果将这些注解应用于受保护、私有或包可见的方法,不会引发错误,但被注解的方法不会表现出配置的缓存设置。如果需要注解非公共方法,可以考虑使用 AspectJ(请参阅本节其余部分),因为它会直接更改字节码。
提示:Spring 建议仅在具体类(及其具体方法)上使用 @Cache*
注解,而不是在接口上使用。当然,可以在接口(或接口方法)上放置 @Cache*
注解,但这仅在使用代理模式(mode = "proxy"
)时有效。如果使用基于织入的切面(mode = "aspectj"
),织入基础结构不会识别接口级别的声明中的缓存设置。
注意:在代理模式(默认)下,只有通过代理进入的外部方法调用才会被拦截。这意味着,自调用(实际上是目标对象中的一个方法调用目标对象的另一个方法)在运行时不会导致实际的缓存操作,即使被调用的方法标记了 @Cacheable
。在这种情况下,可以考虑使用 aspectj
模式。此外,代理必须完全初始化才能提供预期的行为,因此不应该在初始化代码(即 @PostConstruct
)中依赖此功能。
# 3.8、使用自定义注解
# a、自定义注解和 AspectJ
此功能仅适用于基于代理的方法,但通过使用 AspectJ 并进行一些额外的工作也可以启用。
spring - aspects
模块仅为标准注解定义了一个切面。如果定义了自己的注解,也需要为它们定义一个切面。请参考 AnnotationCacheAspect
作为示例。
缓存抽象允许使用自己的注解来标识触发缓存填充或清除的方法。这作为一种模板机制非常方便,因为它消除了重复缓存注解声明的需要,尤其在指定了键或条件,或者代码库中不允许引入外部依赖(org.springframework
)时非常有用。与其他 原型 注解类似,可以将 @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 注解,但容器会在运行时自动获取其声明并理解其含义。请注意,如 前文 所述,需要启用基于注解的缓存行为。
# 4、(JSR - 107) 注解
从 4.1 版本开始,Spring 的缓存抽象全面支持 JCache 标准(JSR - 107)注解:@CacheResult
、@CachePut
、@CacheRemove
和 @CacheRemoveAll
,以及 @CacheDefaults
、@CacheKey
和 @CacheValue
这些辅助注解。即便不将缓存存储迁移到 JSR - 107,你也可以使用这些注解。其内部实现采用了 Spring 的缓存抽象,并提供了符合规范的默认 CacheResolver
和 KeyGenerator
实现。也就是说,如果你已经在使用 Spring 的缓存抽象,那么可以在不改变缓存存储(或相关配置)的情况下,切换到这些标准注解。
# 5、功能概述
对于熟悉 Spring 缓存注解的人来说,下表描述了 Spring 注解与其对应的 JSR - 107 注解之间的主要差异:
Spring 注解 | JSR - 107 注解 | 备注 |
---|---|---|
@Cacheable | @CacheResult | 二者相当相似。@CacheResult 可以缓存特定异常,并能强制执行方法,而不受缓存内容的影响。 |
@CachePut | @CachePut | Spring 使用方法调用的结果更新缓存,而 JCache 要求将需更新的内容作为一个用 @CacheValue 注解标注的参数传入。由于这一差异,JCache 允许在实际方法调用之前或之后更新缓存。 |
@CacheEvict | @CacheRemove | 二者相当相似。@CacheRemove 支持在方法调用抛出异常时进行条件性缓存清除。 |
@CacheEvict(allEntries=true) | @CacheRemoveAll | 参考 @CacheRemove 。 |
@CacheConfig | @CacheDefaults | 二者以相似的方式配置相同的概念。 |
JCache 有 javax.cache.annotation.CacheResolver
的概念,它与 Spring 的 CacheResolver
接口相同,不过 JCache 仅支持单个缓存。默认情况下,一个简单的实现会根据注解中声明的名称来获取要使用的缓存。需要注意的是,如果注解中未指定缓存名称,则会自动生成一个默认名称。更多信息请参考 @CacheResult#cacheName()
的 Java 文档。
CacheResolver
实例由 CacheResolverFactory
获取。可以为每个缓存操作自定义工厂,示例如下:
@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class) // (1)
public Book findBook(ISBN isbn)
- 为此操作自定义工厂。
注意:对于所有引用的类,Spring 会尝试查找具有给定类型的 Bean。如果存在多个匹配项,则会创建一个新实例,并且该实例可以使用常规的 Bean 生命周期回调,例如依赖注入。
键由 javax.cache.annotation.CacheKeyGenerator
生成,其作用与 Spring 的 KeyGenerator
相同。默认情况下,所有方法参数都会被考虑用于生成键,除非至少有一个参数使用 @CacheKey
注解标注。这与 Spring 的自定义键生成声明类似。例如,以下两个操作的效果相同,一个使用 Spring 的抽象,另一个使用 JCache:
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@CacheResult(cacheName="books")
public Book findBook(@CacheKey ISBN isbn, boolean checkWarehouse, boolean includeUsed)
你也可以像指定 CacheResolverFactory
一样,在操作中指定 CacheKeyResolver
。
JCache 可以管理被注解方法抛出的异常。这可以防止更新缓存,还可以将异常作为失败的指示进行缓存,从而避免再次调用该方法。假设如果 ISBN 结构无效会抛出 InvalidIsbnNotFoundException
,这是一种永久性失败(使用这样的参数永远无法获取到书籍)。以下代码将该异常进行缓存,以便后续使用相同的无效 ISBN 进行调用时,直接抛出缓存的异常,而不是再次调用该方法:
@CacheResult(cacheName="books", exceptionCacheName="failures",
cachedExceptions = InvalidIsbnNotFoundException.class)
public Book findBook(ISBN isbn)
# 6、启用 JSR - 107 支持
要在 Spring 的声明式注解支持中启用 JSR - 107 支持,你无需进行特殊操作。如果 JSR - 107 API 和 spring - context - support
模块都在类路径中,@EnableCaching
和 cache:annotation - driven
XML 元素都会自动启用 JCache 支持。
注意:根据你的使用场景,选择基本上由你决定。你甚至可以混合使用,在某些服务上使用 JSR - 107 API,在其他服务上使用 Spring 自己的注解。但是,如果这些服务影响相同的缓存,则应该使用一致且相同的键生成实现。
# 7、基于声明式 XML 的缓存
如果无法使用注解(可能是因为无法访问源代码或者外部代码不支持),你可以使用 XML 进行声明式缓存。因此,你可以不用在方法上添加缓存注解,而是在外部指定目标方法和缓存指令(这与声明式事务管理建议类似)。上一节的示例可以转换为以下示例:
<!-- 我们希望实现可缓存的服务 -->
<bean id="bookService" class="x.y.service.DefaultBookService"/>
<!-- 缓存定义 -->
<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>
<!-- 将可缓存行为应用到所有 BookService 接口 -->
<aop:config>
<aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>
<!-- 省略缓存管理器定义 -->
在上述配置中,bookService
实现了可缓存。要应用的缓存语义封装在 cache:advice
定义中,它使得 findBooks
方法用于将数据存入缓存,loadBooks
方法用于清除数据。这两个定义都作用于 books
缓存。
aop:config
定义通过使用 AspectJ 切入点表达式(更多信息可参考Spring 面向切面编程)将缓存建议应用到程序的适当位置。在上述示例中,会考虑 BookService
中的所有方法,并将缓存建议应用于这些方法。
声明式 XML 缓存支持所有基于注解的模式,因此在两者之间进行切换应该相当容易。此外,两者可以在同一个应用程序中使用。基于 XML 的方法不会触及目标代码,但本质上会更冗长。当处理针对缓存的重载方法时,识别正确的方法需要额外的工作量,因为 method
参数不是一个很好的区分依据。在这些情况下,你可以使用 AspectJ 切入点来挑选目标方法并应用适当的缓存功能。不过,通过 XML 更容易应用包级、组级或接口级的缓存(同样归功于 AspectJ 切入点),也更容易创建类似模板的定义(就像我们在前面的示例中通过 cache:definitions
的 cache
属性定义目标缓存那样)。
# 8、配置缓存存储
缓存抽象提供了多种存储集成选项。要使用这些选项,你需要声明一个合适的 CacheManager
(这是一个控制和管理 Cache
实例的实体,可用于从存储中检索这些实例)。
# 8.1、基于 JDK ConcurrentMap
的缓存
基于 JDK 的 Cache
实现位于 org.springframework.cache.concurrent
包下。它允许你使用 ConcurrentHashMap
作为缓存的存储后端。以下示例展示了如何配置两个缓存:
@Bean
ConcurrentMapCacheFactoryBean defaultCache() {
ConcurrentMapCacheFactoryBean cache = new ConcurrentMapCacheFactoryBean();
cache.setName("default");
return cache;
}
@Bean
ConcurrentMapCacheFactoryBean booksCache() {
ConcurrentMapCacheFactoryBean cache = new ConcurrentMapCacheFactoryBean();
cache.setName("books");
return cache;
}
@Bean
CacheManager cacheManager(ConcurrentMapCache defaultCache, ConcurrentMapCache booksCache) {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Set.of(defaultCache, booksCache));
return cacheManager;
}
<!-- 简单的缓存管理器 -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" name="default"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" name="books"/>
</set>
</property>
</bean>
上述代码片段使用 SimpleCacheManager
为两个名为 default
和 books
的 ConcurrentMapCache
实例创建了一个 CacheManager
。请注意,每个缓存的名称是直接配置的。
由于该缓存由应用程序创建,它与应用程序的生命周期绑定,因此适用于基本用例、测试或简单的应用程序。该缓存具有良好的扩展性且速度极快,但它不提供任何管理、持久化功能或缓存清除策略。
# 8.2、基于 Ehcache 的缓存
Ehcache 3.x 完全符合 JSR - 107 标准,无需专门的支持。有关详细信息,请参阅JSR - 107 缓存。
# 8.3、缓存
Caffeine 是 Guava 缓存的 Java 8 重写版本,其实现位于 org.springframework.cache.caffeine
包下,并提供了对 Caffeine 多项特性的访问。
以下示例配置了一个按需创建缓存的 CacheManager
:
@Bean
CacheManager cacheManager() {
return new CaffeineCacheManager();
}
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager"/>
你也可以显式指定要使用的缓存。在这种情况下,管理器只会提供这些显式指定的缓存。以下示例展示了如何实现:
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheNames(List.of("default", "books"));
return cacheManager;
}
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
<property name="cacheNames">
<set>
<value>default</value>
<value>books</value>
</set>
</property>
</bean>
Caffeine CacheManager
还支持自定义 Caffeine
和 CacheLoader
。有关这些内容的更多信息,请参阅 Caffeine 文档 (opens new window)。
# 8.4、基于 GemFire 的缓存
GemFire 是一个面向内存、支持磁盘备份、可弹性扩展、持续可用、主动(内置基于模式的订阅通知)且全局复制的数据库,并提供了功能齐全的边缘缓存。有关如何将 GemFire 用作 CacheManager
(及更多内容)的详细信息,请参阅 Spring Data GemFire 参考文档 (opens new window)。
# 8.5、- 107 缓存
Spring 的缓存抽象也可以使用符合 JSR - 107 标准的缓存。JCache 实现位于 org.springframework.cache.jcache
包下。
同样,要使用它,你需要声明合适的 CacheManager
。以下示例展示了如何操作:
@Bean
javax.cache.CacheManager jCacheManager() {
CachingProvider cachingProvider = Caching.getCachingProvider();
return cachingProvider.getCacheManager();
}
@Bean
org.springframework.cache.CacheManager cacheManager(javax.cache.CacheManager jCacheManager) {
return new JCacheCacheManager(jCacheManager);
}
<bean id="cacheManager"
class="org.springframework.cache.jcache.JCacheCacheManager"
p:cache - manager - ref="jCacheManager"/>
<!-- JSR - 107 缓存管理器设置 -->
<bean id="jCacheManager" .../>
# 8.6、处理无后端存储的缓存
有时,在切换环境或进行测试时,你可能声明了缓存,但没有配置实际的后端缓存。由于这是一个无效的配置,运行时会抛出异常,因为缓存基础设施无法找到合适的存储。在这种情况下,与其删除缓存声明(这可能很繁琐),你可以使用一个简单的虚拟缓存,该缓存不执行任何缓存操作,即每次都强制调用缓存方法。以下示例展示了如何实现:
@Bean
CacheManager cacheManager(CacheManager jdkCache, CacheManager gemfireCache) {
CompositeCacheManager cacheManager = new CompositeCacheManager();
cacheManager.setCacheManagers(List.of(jdkCache, gemfireCache));
cacheManager.setFallbackToNoOpCache(true);
return cacheManager;
}
<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
标志为所有未由已配置的缓存管理器处理的定义添加了一个无操作缓存。也就是说,任何在 jdkCache
或 gemfireCache
(在该示例中之前已配置)中未找到的缓存定义,都将由无操作缓存处理,该缓存不存储任何信息,从而导致每次都调用目标方法。
# 9、接入不同的后端缓存
显然,市面上有很多缓存产品可供你用作后备存储。对于那些不支持 JSR - 107 的产品,你需要提供一个 CacheManager
和一个 Cache
实现。这听起来可能比实际要难,因为在实践中,这些类往往是简单的 适配器 (opens new window),它们将缓存抽象框架映射到存储 API 之上,就像 Caffeine 类所做的那样。大多数 CacheManager
类可以使用 org.springframework.cache.support
包中的类(例如 AbstractCacheManager
,它会处理样板代码,你只需完成实际的映射)。
# 10、如何设置TTL/TTI/淘汰策略等功能?
直接通过你的缓存提供程序进行设置。缓存抽象只是一种抽象概念,并非具体的缓存实现。你所采用的解决方案可能支持多种数据策略和不同的拓扑结构,而其他方案可能并不支持这些(例如,JDK中的ConcurrentHashMap
—— 在缓存抽象中暴露这些并无实际意义,因为没有相应的底层支持)。此类功能应通过底层缓存(在配置时)或其原生API直接进行控制。
# 七、可观测性支持
Micrometer 定义了一种观察(Observation)概念,可在应用程序中同时启用指标和追踪 (opens new window)。指标支持为创建计时器、仪表或计数器提供了一种方式,用于收集应用程序运行时行为的统计信息。指标可以帮助你跟踪错误率、使用模式、性能等等。追踪则提供了整个系统的全面视图,跨越应用程序边界;你可以深入查看特定的用户请求,并跟踪它们在各个应用程序中的完整执行过程。
如果配置了 ObservationRegistry
,Spring 框架会对其自身代码库的各个部分进行检测,以发布观察信息。你可以进一步了解在 Spring Boot 中配置可观测性基础架构 (opens new window)。
# 1、生成的观察信息列表
Spring 框架对多个功能进行了可观测性检测。正如本节开头所述,观察信息可以根据配置生成计时器指标和/或追踪信息。
观察信息名称 | 描述 |
---|---|
"http.client.requests" | HTTP 客户端交换所花费的时间 |
"http.server.requests" | 框架层面处理 HTTP 服务器交换的时间 |
"jms.message.publish" | 消息生产者向目标发送 JMS 消息所花费的时间 |
"jms.message.process" | 消息消费者接收的 JMS 消息的处理时间 |
"tasks.scheduled.execution" | @Scheduled 任务的执行处理时间 |
注意:观察信息使用的是 Micrometer 的官方命名约定,但指标名称将根据监控系统后端(如 Prometheus、Atlas、Graphite、InfluxDB 等)的格式要求,自动进行转换 (opens new window)。
# 2、观察概念
如果你不熟悉 Micrometer 观察,可以了解以下相关概念。
Observation
是对应用程序中正在发生的事情的实际记录。它由ObservationHandler
实现类处理,以生成指标或追踪信息。- 每个观察都有一个对应的
ObservationContext
实现类;该类保存了提取其元数据的所有相关信息。对于 HTTP 服务器观察,上下文实现类可能包含 HTTP 请求、HTTP 响应以及处理过程中抛出的任何异常等信息。 - 每个
Observation
都包含KeyValues
元数据。以 HTTP 服务器观察为例,这些元数据可能包括 HTTP 请求方法、HTTP 响应状态等。这些元数据由ObservationConvention
实现类提供,这些实现类应该声明它们支持的ObservationContext
类型。 - 如果
KeyValue
元组的可能值数量较少且有限,则称这些KeyValues
为 “低基数”。例如,HTTP 方法就是一个很好的例子。低基数的值仅用于生成指标。相反,“高基数” 的值是无界的(例如,HTTP 请求 URI),仅用于生成追踪信息。 ObservationDocumentation
记录了特定领域中的所有观察信息,列出了预期的键名及其含义。
# 3、配置观察信息
全局配置选项在 ObservationRegistry#observationConfig()
级别提供。每个仪器化组件将提供两个扩展点:
- 设置
ObservationRegistry
;如果未设置,则观察信息不会被记录,操作将被忽略。 - 提供自定义的
ObservationConvention
,以更改默认的观察名称和提取的KeyValues
。
# 3.1、使用自定义观察约定
我们以 Spring MVC 的 “http.server.requests” 指标检测为例,使用 ServerHttpObservationFilter
。这个观察使用了一个带有 ServerRequestObservationContext
的 ServerRequestObservationConvention
;你可以在 Servlet 过滤器上配置自定义约定。如果你想要自定义观察生成的元数据,可以根据自己的需求扩展 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) {
// 在此,我们仅向观察中添加一个额外的 KeyValue,并保留默认值
return super.getLowCardinalityKeyValues(context).and(custom(context));
}
private KeyValue custom(ServerRequestObservationContext context) {
return KeyValue.of("custom.method", context.getCarrier().getMethod());
}
}
如果你想要完全控制观察的所有参数,可以实现你感兴趣的观察的整个约定合同:
import java.util.Locale;
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() {
// 将用作指标名称
return "http.server.requests";
}
@Override
public String getContextualName(ServerRequestObservationContext context) {
// 将用作追踪名称
return "http " + context.getCarrier().getMethod().toLowerCase(Locale.ROOT);
}
@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) {
// 你应该尽可能重用相应的 ObservationDocumentation 中的键名
return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod());
}
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
来实现类似的目标,即为观察添加或删除键值。过滤器不会替代默认约定,而是作为后处理组件使用。
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
实例。
# 4、@Scheduled
任务检测
会为每次执行的 @Scheduled
任务创建观察信息。应用程序需要在 ScheduledTaskRegistrar
上配置 ObservationRegistry
,以启用观察信息的记录。这可以通过声明一个设置观察注册表的 SchedulingConfigurer
bean 来完成:
import io.micrometer.observation.ObservationRegistry;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
public class ObservationSchedulingConfigurer implements SchedulingConfigurer {
private final ObservationRegistry observationRegistry;
public ObservationSchedulingConfigurer(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setObservationRegistry(this.observationRegistry);
}
}
默认情况下,它使用 org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention
,并由 ScheduledTaskObservationContext
支持。你可以直接在 ObservationRegistry
上配置自定义实现。在执行计划方法期间,当前的观察信息会恢复到 ThreadLocal
上下文或 Reactor 上下文(如果计划方法返回 Mono
或 Flux
类型)。
默认情况下,会创建以下 KeyValues
:
名称 | 描述 |
---|---|
code.function (必需) | 计划执行的 Java Method 的名称。 |
code.namespace (必需) | 持有计划方法的 bean 实例的类的规范名称,如果是匿名类,则为 "ANONYMOUS" 。 |
error (必需) | 执行期间抛出的异常的类名,如果没有异常发生,则为 "none" 。 |
exception (已弃用) | 重复 error 键,将来可能会被移除。 |
outcome (必需) | 方法执行的结果。可以是 "SUCCESS" 、"ERROR" 或 "UNKNOWN" (例如,如果操作在执行期间被取消)。 |
# 5、消息检测
如果类路径中存在 io.micrometer:micrometer - jakarta9
依赖项,Spring 框架会使用 Micrometer 提供的 Jakarta JMS 检测功能。io.micrometer.jakarta9.instrument.jms.JmsInstrumentation
会对 jakarta.jms.Session
进行检测,并记录相关的观察信息。
这个检测功能将创建两种类型的观察信息:
“jms.message.publish”
:当使用JmsTemplate
将 JMS 消息发送到代理时记录。“jms.message.process”
:当使用MessageListener
或@JmsListener
注解的方法处理 JMS 消息时记录。
注意:目前没有对 “jms.message.receive”
观察信息进行检测,因为测量等待接收消息所花费的时间没有太大价值。这样的集成通常会对 MessageConsumer#receive
方法调用进行检测,但一旦这些方法返回,处理时间将无法测量,追踪范围也无法传播到应用程序。
默认情况下,这两种观察信息共享相同的一组可能的 KeyValues
:
# 5.1、低基数键
名称 | 描述 |
---|---|
error | 消息操作期间抛出的异常的类名(或 "none" )。 |
exception (已弃用) | 重复 error 键,将来可能会被移除。 |
messaging.destination.temporary (必需) | 目标是否为 TemporaryQueue 或 TemporaryTopic (值:"true" 或 "false" )。 |
messaging.operation (必需) | 正在执行的 JMS 操作的名称(值:"publish" 或 "process" )。 |
# 5.2、高基数键
名称 | 描述 |
---|---|
messaging.message.conversation_id | JMS 消息的关联 ID。 |
messaging.destination.name | 当前消息发送到的目标的名称。 |
messaging.message.id | 消息系统用作消息标识符的值。 |
# 5.3、消息发布检测
当使用 JmsTemplate
将 JMS 消息发送到代理时,会记录 “jms.message.publish”
观察信息。这些观察信息用于测量发送消息所花费的时间,并通过传出的 JMS 消息头传播跟踪信息。
你需要在 JmsTemplate
上配置 ObservationRegistry
以启用观察:
import io.micrometer.observation.ObservationRegistry;
import jakarta.jms.ConnectionFactory;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.jms.core.JmsTemplate;
public class JmsTemplatePublish {
private final JmsTemplate jmsTemplate;
private final JmsMessagingTemplate jmsMessagingTemplate;
public JmsTemplatePublish(ObservationRegistry observationRegistry, ConnectionFactory connectionFactory) {
this.jmsTemplate = new JmsTemplate(connectionFactory);
// 配置观察注册表
this.jmsTemplate.setObservationRegistry(observationRegistry);
// 对于 JmsMessagingTemplate,使用配置了注册表的 JMS 模板实例化它
this.jmsMessagingTemplate = new JmsMessagingTemplate(this.jmsTemplate);
}
public void sendMessages() {
this.jmsTemplate.convertAndSend("spring.observation.test", "test message");
}
}
默认情况下,它使用 io.micrometer.jakarta9.instrument.jms.DefaultJmsPublishObservationConvention
,由 io.micrometer.jakarta9.instrument.jms.JmsPublishObservationContext
提供支持。
当使用 @JmsListener
注解的方法返回响应消息时,也会记录类似的观察信息。
# 5.4、消息处理检测
当应用程序处理 JMS 消息时,会记录 “jms.message.process”
观察信息。这些观察信息用于衡量处理消息所花费的时间,并通过传入的 JMS 消息头传播追踪上下文。
大多数应用程序将使用由 @JmsListener
注解的方法机制来处理传入的消息。你需要确保在专门的 JmsListenerContainerFactory
上配置了 ObservationRegistry
:
import io.micrometer.observation.ObservationRegistry;
import jakarta.jms.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
@Configuration
@EnableJms
public class JmsConfiguration {
@Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, ObservationRegistry observationRegistry) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setObservationRegistry(observationRegistry);
return factory;
}
}
必须配置一个默认的容器工厂以启用注解支持,但请注意,@JmsListener
注解可以引用特定的容器工厂 bean 以实现特定目的。在所有情况下,只有在容器工厂上配置了观察注册表时,才会记录观察信息。
当使用 JmsTemplate
处理消息时,也会记录类似的观察信息。此类监听器在会话回调中设置在 MessageConsumer
上(请参见 JmsTemplate.execute(SessionCallback<T>)
)。
默认情况下,此观察使用 io.micrometer.jakarta9.instrument.jms.DefaultJmsProcessObservationConvention
,由 io.micrometer.jakarta9.instrument.jms.JmsProcessObservationContext
支持。
# 6、服务器检测
对于 Servlet 和响应式应用程序,HTTP 服务器交换观察信息以 “http.server.requests”
为名称创建。
# 6.1、应用程序
应用程序需要在其应用程序中配置 org.springframework.web.filter.ServerHttpObservationFilter
Servlet 过滤器。默认情况下,它使用 org.springframework.http.server.observation.DefaultServerRequestObservationConvention
,由 ServerRequestObservationContext
提供支持。
只有当 Exception
未被 Web 框架处理并冒泡到 Servlet 过滤器时,观察信息才会记录为错误。通常,由 Spring MVC 的 @ExceptionHandler
和 ProblemDetail
支持处理的所有异常不会在观察信息中记录。在请求处理的任何时候,你可以自己在 ObservationContext
上设置错误字段:
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) {
// 我们希望将此异常记录在观察信息中
ServerHttpObservationFilter.findObservationContext(request)
.ifPresent(context -> context.setError(exception));
return ResponseEntity.notFound().build();
}
static class MissingUserException extends RuntimeException {
}
}
注意:由于检测是在 Servlet 过滤器级别完成的,观察范围仅涵盖此过滤器之后排序的过滤器以及请求的处理。通常,Servlet 容器错误处理在较低级别执行,不会有任何活动的观察或跨越范围。对于这种情况,需要特定容器的实现,例如 Tomcat 的 org.apache.catalina.Valve
;这超出了本项目的范围。
默认情况下,会创建以下 KeyValues
:
# 6.2、低基数键
名称 | 描述 |
---|---|
error (必需) | 交换期间抛出的异常的类名,如果没有异常发生,则为 "none" 。 |
exception (已弃用) | 重复 error 键,将来可能会被移除。 |
method (必需) | HTTP 请求方法的名称,如果不是知名方法,则为 "none" 。 |
outcome (必需) | HTTP 服务器交换的结果。 |
status (必需) | HTTP 响应的原始状态代码,如果未创建响应,则为 "UNKNOWN" 。 |
uri (必需) | 如果可用,匹配处理程序的 URI 模式;对于 3xx 响应,回退为 REDIRECTION ;对于 404 响应,回退为 NOT_FOUND ;对于没有路径信息的请求,回退为 root ;对于所有其他请求,回退为 UNKNOWN 。 |
# 6.3、高基数键
名称 | 描述 |
---|---|
http.url (必需) | HTTP 请求的 URI。 |
# 6.4、响应式应用程序
应用程序需要使用 MeterRegistry
配置 WebHttpHandlerBuilder
,以启用服务器检测。可以在 WebHttpHandlerBuilder
上完成此操作,如下所示:
import io.micrometer.observation.ObservationRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
@Configuration(proxyBeanMethods = false)
public class HttpHandlerConfiguration {
private final ApplicationContext applicationContext;
public HttpHandlerConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Bean
public HttpHandler httpHandler(ObservationRegistry registry) {
return WebHttpHandlerBuilder.applicationContext(this.applicationContext)
.observationRegistry(registry)
.build();
}
}
默认情况下,它使用 org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention
,由 ServerRequestObservationContext
提供支持。
只有当 Exception
未被应用程序控制器处理时,观察信息才会记录为错误。通常,由 Spring WebFlux 的 @ExceptionHandler
和 ProblemDetail
支持处理的所有异常不会在观察信息中记录。在请求处理的任何时候,你可以自己在 ObservationContext
上设置错误字段:
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ServerWebExchange;
@Controller
public class UserController {
@ExceptionHandler(MissingUserException.class)
ResponseEntity<Void> handleMissingUser(ServerWebExchange exchange, MissingUserException exception) {
// 我们希望将此异常记录在观察信息中
ServerRequestObservationContext.findCurrent(exchange.getAttributes())
.ifPresent(context -> context.setError(exception));
return ResponseEntity.notFound().build();
}
static class MissingUserException extends RuntimeException {
}
}
默认情况下,会创建以下 KeyValues
:
# 6.5、低基数键
名称 | 描述 |
---|---|
error (必需) | 交换期间抛出的异常的类名,如果没有异常发生,则为 "none" 。 |
exception (已弃用) | 重复 error 键,将来可能会被移除。 |
method (必需) | HTTP 请求方法的名称,如果不是知名方法,则为 "none" 。 |
outcome (必需) | HTTP 服务器交换的结果。 |
status (必需) | HTTP 响应的原始状态代码,如果未创建响应,则为 "UNKNOWN" 。 |
uri (必需) | 如果可用,匹配处理程序的 URI 模式;对于 3xx 响应,回退为 REDIRECTION ;对于 404 响应,回退为 NOT_FOUND ;对于没有路径信息的请求,回退为 root ;对于所有其他请求,回退为 UNKNOWN 。 |
# 6.6、高基数键
名称 | 描述 |
---|---|
http.url (必需) | HTTP 请求的 URI。 |
# 7、客户端检测
对于阻塞和响应式客户端,HTTP 客户端交换观察信息以 “http.client.requests”
为名称创建。此观察信息衡量整个 HTTP 请求/响应交换,从建立连接到主体反序列化。与服务器端的检测不同,此检测直接在客户端实现,因此唯一需要的步骤是在客户端上配置 ObservationRegistry
。
# 7.1、RestTemplate
应用程序必须在 RestTemplate
实例上配置 ObservationRegistry
以启用检测;否则,观察信息将是无操作的。Spring Boot 会自动配置 RestTemplateBuilder
bean 并设置观察注册表。
检测默认使用 org.springframework.http.client.observation.ClientRequestObservationConvention
,由 ClientRequestObservationContext
支持。
# 7.2、低基数键
名称 | 描述 |
---|---|
method (必需) | HTTP 请求方法的名称,如果不是知名方法,则为 "none" 。 |
uri (必需) | 用于 HTTP 请求的 URI 模板,如果未提供,则为 "none" 。不考虑 URI 的协议、主机和端口部分。 |
client.name (必需) | 从请求 URI 主机派生的客户端名称。 |
status (必需) | HTTP 响应的原始状态代码;如果发生 IOException ,则为 "IO_ERROR" ;如果未收到响应,则为 "CLIENT_ERROR" 。 |
outcome (必需) | HTTP 客户端交换的结果。 |
error (必需) | 交换期间抛出的异常的类名,如果没有异常发生,则为 "none" 。 |
exception (已弃用) | 重复 error 键,将来可能会被移除。 |
# 7.3、高基数键
名称 | 描述 |
---|---|
http.url (必需) | HTTP 请求的 URI。 |
# 7.4、RestClient
应用程序必须在 RestClient.Builder
上配置 ObservationRegistry
以启用检测;否则,观察信息将是无操作的。
检测默认使用 org.springframework.http.client.observation.ClientRequestObservationConvention
,由 ClientRequestObservationContext
支持。
# 7.5、低基数键
名称 | 描述 |
---|---|
method (必需) | HTTP 请求方法的名称,如果请求无法创建,则为 "none" 。 |
uri (必需) | 用于 HTTP 请求的 URI 模板,如果未提供,则为 "none" 。不考虑 URI 的协议、主机和端口部分。 |
client.name (必需) | 从请求 URI 主机派生的客户端名称。 |
status (必需) | HTTP 响应的原始状态代码;如果发生 IOException ,则为 "IO_ERROR" ;如果未收到响应,则为 "CLIENT_ERROR" 。 |
outcome (必需) | HTTP 客户端交换的结果。 |
error (必需) | 交换期间抛出的异常的类名,如果没有异常发生,则为 "none" 。 |
exception (已弃用) | 重复 error 键,将来可能会被移除。 |
# 7.6、高基数键
名称 | 描述 |
---|---|
http.url (必需) | HTTP 请求的 URI。 |
# 7.7、WebClient
应用程序必须在 WebClient.Builder
上配置 ObservationRegistry
以启用检测;否则,观察信息将是无操作的。Spring Boot 会自动配置 WebClient.Builder
bean 并设置观察注册表。
检测默认使用 org.springframework.web.reactive.function.client.ClientRequestObservationConvention
,由 ClientRequestObservationContext
支持。
# 7.8、低基数键
名称 | 描述 |
---|---|
method (必需) | HTTP 请求方法的名称,如果不是知名方法,则为 "none" 。 |
uri (必需) | 用于 HTTP 请求的 URI 模板,如果未提供,则为 "none" 。不考虑 URI 的协议、主机和端口部分。 |
client.name (必需) | 从请求 URI 主机派生的客户端名称。 |
status (必需) | HTTP 响应的原始状态代码;如果发生 IOException ,则为 "IO_ERROR" ;如果未收到响应,则为 "CLIENT_ERROR" 。 |
outcome (必需) | HTTP 客户端交换的结果。 |
error (必需) | 交换期间抛出的异常的类名,如果没有异常发生,则为 "none" 。 |
exception (已弃用) | 重复 error 键,将来可能会被移除。 |
# 7.9、高基数键
名称 | 描述 |
---|---|
http.url (必需) | HTTP 请求的 URI。 |
# 8、应用程序事件和 @EventListener
Spring 框架不会为 @EventListener
调用提供观察信息,因为它们不具备此类检测所需的正确语义。默认情况下,事件发布和处理是在同一线程上同步完成的。这意味着在执行该任务期间,ThreadLocals 和日志记录上下文将与事件发布者相同。
如果应用程序全局配置了一个自定义的 ApplicationEventMulticaster
,其策略是在不同的线程上调度事件处理,那么情况就不同了。所有 @EventListener
方法都将在不同的线程上处理,而不是在主事件发布线程上。在这些情况下,[Micrometer 上下文传播库](https://docs.micrometer.io/context - propagation/reference/)可以帮助传播此类值,并更好地关联事件的处理。应用程序可以配置选择的 TaskExecutor
,以使用 ContextPropagatingTaskDecorator
来修饰任务并传播上下文。要使这一功能生效,类路径中必须存在 io.micrometer:context - propagation
库:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
@Configuration
public class ApplicationEventsConfiguration {
@Bean(name = "applicationEventMulticaster")
public SimpleApplicationEventMulticaster simpleApplicationEventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
// 使用支持上下文传播的装饰器修饰任务执行
taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator());
eventMulticaster.setTaskExecutor(taskExecutor);
return eventMulticaster;
}
}
同样,如果为每个 @EventListener
注解的方法单独选择了异步处理,即添加了 @Async
注解,你可以选择一个能够传播上下文的 TaskExecutor
,通过其限定符来引用它。假设以下 TaskExecutor
bean 的定义,并配置了专用的任务装饰器:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
@Configuration
public class EventAsyncExecutionConfiguration {
@Bean(name = "propagatingContextExecutor")
public TaskExecutor propagatingContextExecutor() {
SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
// 使用支持上下文传播的装饰器修饰任务执行
taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator());
return taskExecutor;
}
}
使用 @Async
注解和相关的限定符来注解事件监听器,会实现类似的上下文传播效果:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class EmailNotificationListener {
private final Log logger = LogFactory.getLog(EmailNotificationListener.class);
@EventListener(EmailReceivedEvent.class)
@Async("propagatingContextExecutor")
public void emailReceived(EmailReceivedEvent event) {
// 异步处理接收到的事件
// 此日志记录语句将包含从传播上下文中获得的预期 MDC 条目
logger.info("email has been received");
}
}
# 八、检查点恢复
Spring 框架与由项目 CRaC (opens new window) 实现的检查点/恢复功能集成,以便让基于 Spring 的 Java 应用程序借助 JVM 实现减少启动和预热时间的系统。
使用此功能需要满足以下条件:
- 启用了检查点/恢复功能的 JVM(目前仅支持 Linux)。
- 类路径中存在 org.crac:crac (opens new window) 库(支持版本 1.4.0 及以上)。
- 指定所需的
java
命令行参数,如-XX:CRaCCheckpointTo=PATH
或-XX:CRaCRestoreFrom=PATH
。
警告:当请求创建检查点时,-XX:CRaCCheckpointTo=PATH
指定路径中生成的文件包含正在运行的 JVM 的内存表示,其中可能包含机密信息和其他敏感数据。使用此功能时应假定 JVM “看到” 的任何值(例如来自环境的配置属性)都将存储在这些 CRaC 文件中。因此,应仔细评估这些文件的生成位置、存储方式和访问方式所带来的安全影响。
从概念上讲,检查点和恢复与单个 Bean 的 Spring Lifecycle
契约一致。
# 1、运行中应用程序的按需检查点/恢复
可以按需创建检查点,例如使用 jcmd application.jar JDK.checkpoint
这样的命令。在创建检查点之前,Spring 会停止所有正在运行的 Bean,通过实现 Lifecycle.stop
方法为它们提供关闭资源的机会。恢复之后,同样的 Bean 会重新启动,Lifecycle.start
方法允许 Bean 在需要时重新打开资源。对于不依赖 Spring 的库,可以通过实现 org.crac.Resource
接口并注册相关实例来提供自定义的检查点/恢复集成。
警告:利用运行中应用程序的检查点/恢复功能通常需要额外的生命周期管理,以便优雅地停止和启动对文件或套接字等资源的使用,并停止活动线程。
警告:请注意,当以固定速率定义调度任务时,例如使用 @Scheduled(fixedRate = 5000)
这样的注解,在按需检查点/恢复的 JVM 恢复时,会执行检查点到恢复之间错过的所有任务。如果这不是你想要的行为,建议以固定延迟(例如使用 @Scheduled(fixedDelay = 5000)
)或使用 cron 表达式来调度任务,因为这些方式是在每次任务执行后计算的。
注意:如果在预热后的 JVM 上创建检查点,恢复后的 JVM 也同样是预热过的,可能立即就能达到峰值性能。这种方法通常需要访问远程服务,因此需要一定程度的平台集成。
# 2、启动时的自动检查点/恢复
当设置 -Dspring.context.checkpoint=onRefresh
JVM 系统属性时,会在启动期间的 LifecycleProcessor.onRefresh
阶段自动创建检查点。此阶段完成后,所有非懒加载的单例 Bean 都已实例化,并且 InitializingBean#afterPropertiesSet
回调已被调用;但生命周期尚未开始,ContextRefreshedEvent
还未发布。
出于测试目的,也可以使用 -Dspring.context.exit=onRefresh
JVM 系统属性,它会触发类似的行为,但不是创建检查点,而是在相同的生命周期阶段退出 Spring 应用程序,且不需要依赖 Project CraC 或在 Linux 系统上运行。这有助于检查在 Bean 未启动时是否需要连接远程服务,并可能优化配置以避免这种情况。
警告:如上所述,特别是在将 CRaC 文件作为可部署工件(例如容器镜像)的一部分进行分发的用例中,应假定 JVM “看到” 的任何敏感数据最终都会存储在 CRaC 文件中,并仔细评估相关的安全影响。
注意:自动检查点/恢复是一种将应用程序启动“快进”到应用程序上下文即将启动阶段的方法,但它无法让 JVM 完全预热。
# 九、CDS
类数据共享(Class Data Sharing,CDS)是一项 JVM 特性 (opens new window),有助于减少 Java 应用程序的启动时间和内存占用。
要使用此特性,需要为应用程序的特定类路径创建一个 CDS 存档。Spring 框架提供了一个切入点,以简化存档的创建过程。一旦存档可用,用户可通过 JVM 标志选择使用它。
# 1、创建 CDS 存档
应用程序的 CDS 存档可在应用程序退出时创建。Spring 框架提供了一种操作模式,当 ApplicationContext
刷新后,进程会自动退出。在这种模式下,所有非懒加载的单例都已实例化,并且 InitializingBean#afterPropertiesSet
回调已被调用;但生命周期尚未启动,ContextRefreshedEvent
也尚未发布。
要创建存档,必须指定两个额外的 JVM 标志:
-XX:ArchiveClassesAtExit=application.jsa
:在退出时创建 CDS 存档-Dspring.context.exit=onRefresh
:按上述方式启动 Spring 应用程序,然后立即退出
要创建 CDS 存档,你的 JDK/JRE 必须有一个基础镜像。如果将上述标志添加到启动脚本中,可能会看到如下警告:
-XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded. Run with -Xlog:cds for more info.
基础 CDS 存档通常是开箱即用的,但如果需要,也可通过执行以下命令创建:
$ java -Xshare:dump
# 2、使用存档
存档可用后,若工作目录中有 application.jsa
文件,可将 -XX:SharedArchiveFile=application.jsa
添加到启动脚本中以使用该存档。
要检查 CDS 缓存是否有效,可使用 -Xshare:on
(仅用于测试,切勿用于生产环境),若无法启用 CDS,该命令会打印错误信息并退出。
要了解缓存的有效性,可通过添加额外属性 -Xlog:class+load:file=cds.log
来启用类加载日志。这将创建一个 cds.log
文件,其中记录了每次加载类的尝试及其来源。从缓存加载的类的来源应为 “shared objects file”,示例如下:
[0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top)
[0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top)
如果无法启用 CDS,或者有大量类未从缓存加载,请确保在创建和使用存档时满足以下条件:
- 必须使用完全相同的 JVM。
- 类路径必须指定为 JAR 文件列表,避免使用目录和
*
通配符。 - 必须保留 JAR 文件的时间戳。
- 使用存档时,类路径必须与创建存档时使用的类路径相同,且顺序一致。额外的 JAR 文件或目录可以 在末尾 指定(但不会被缓存)。
# 十、附录
# 1、XML 架构
本附录的这一部分列出了与集成技术相关的 XML 架构。
# 1.1、“jee” 架构
“jee” 元素用于处理与 Jakarta EE(企业版)配置相关的问题,例如查找 JNDI 对象和定义 EJB 引用。
要使用 “jee” 架构中的元素,需要在 Spring XML 配置文件的顶部添加以下前缀。以下代码片段中的文本引用了正确的架构,这样 “jee” 命名空间中的元素就可以使用了:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee
https://www.springframework.org/schema/jee/spring-jee.xsd">
<!-- 这里是 Bean 定义 -->
</beans>
# a、<jee:jndi-lookup/>
(简单示例)
以下示例展示了在不使用 “jee” 架构的情况下如何使用 JNDI 查找数据源:
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/MyDataSource"/>
</bean>
<bean id="userDao" class="com.foo.JdbcUserDao">
<!-- Spring 会像往常一样自动进行类型转换 -->
<property name="dataSource" ref="dataSource"/>
</bean>
以下示例展示了使用 “jee” 架构时如何使用 JNDI 查找数据源:
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource"/>
<bean id="userDao" class="com.foo.JdbcUserDao">
<!-- Spring 会像往常一样自动进行类型转换 -->
<property name="dataSource" ref="dataSource"/>
</bean>
# b、<jee:jndi-lookup/>
(单个 JNDI 环境设置)
以下示例展示了在不使用 “jee” 的情况下如何使用 JNDI 查找环境变量:
<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>
以下示例展示了使用 “jee” 时如何使用 JNDI 查找环境变量:
<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
<jee:environment>ping=pong</jee:environment>
</jee:jndi-lookup>
# c、<jee:jndi-lookup/>
(多个 JNDI 环境设置)
以下示例展示了在不使用 “jee” 的情况下如何使用 JNDI 查找多个环境变量:
<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>
以下示例展示了使用 “jee” 时如何使用 JNDI 查找多个环境变量:
<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource">
<!-- 换行分隔的环境键值对(标准属性格式) -->
<jee:environment>
sing=song
ping=pong
</jee:environment>
</jee:jndi-lookup>
# d、<jee:jndi-lookup/>
(复杂示例)
以下示例展示了在不使用 “jee” 的情况下如何使用 JNDI 查找数据源和多个不同属性:
<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>
以下示例展示了使用 “jee” 时如何使用 JNDI 查找数据源和多个不同属性:
<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"/>
# e、<jee:local-slsb/>
(简单示例)
<jee:local-slsb/>
元素用于配置对本地 EJB 无状态会话 Bean 的引用。
以下示例展示了在不使用 “jee” 的情况下如何配置对本地 EJB 无状态会话 Bean 的引用:
<bean id="simple"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/RentalServiceBean"/>
<property name="businessInterface" value="com.foo.service.RentalService"/>
</bean>
以下示例展示了使用 “jee” 时如何配置对本地 EJB 无状态会话 Bean 的引用:
<jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean"
business-interface="com.foo.service.RentalService"/>
# f、<jee:local-slsb/>
(复杂示例)
<jee:local-slsb/>
元素用于配置对本地 EJB 无状态会话 Bean 的引用。
以下示例展示了在不使用 “jee” 的情况下如何配置对本地 EJB 无状态会话 Bean 和多个属性的引用:
<bean id="complexLocalEjb"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/RentalServiceBean"/>
<property name="businessInterface" value="com.example.service.RentalService"/>
<property name="cacheHome" value="true"/>
<property name="lookupHomeOnStartup" value="true"/>
<property name="resourceRef" value="true"/>
</bean>
以下示例展示了使用 “jee” 时如何配置对本地 EJB 无状态会话 Bean 和多个属性的引用:
<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"/>
# g、<jee:remote-slsb/>
<jee:remote-slsb/>
元素用于配置对 “远程” EJB 无状态会话 Bean 的引用。
以下示例展示了在不使用 “jee” 的情况下如何配置对远程 EJB 无状态会话 Bean 的引用:
<bean id="complexRemoteEjb"
class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean">
<property name="jndiName" value="ejb/MyRemoteBean"/>
<property name="businessInterface" value="com.foo.service.RentalService"/>
<property name="cacheHome" value="true"/>
<property name="lookupHomeOnStartup" value="true"/>
<property name="resourceRef" value="true"/>
<property name="homeInterface" value="com.foo.service.RentalService"/>
<property name="refreshHomeOnConnectFailure" value="true"/>
</bean>
以下示例展示了使用 “jee” 时如何配置对远程 EJB 无状态会话 Bean 的引用:
<jee:remote-slsb id="complexRemoteEjb"
jndi-name="ejb/MyRemoteBean"
business-interface="com.foo.service.RentalService"
cache-home="true"
lookup-home-on-startup="true"
resource-ref="true"
home-interface="com.foo.service.RentalService"
refresh-home-on-connect-failure="true"/>
# 1.2、“jms” 架构
“jms” 元素用于配置与 JMS 相关的 Bean,例如 Spring 的消息监听器容器。这些元素的详细信息在JMS 章节的JMS 命名空间支持部分中介绍。有关此支持和 “jms” 元素本身的完整详细信息,请参阅该章节。
为了完整起见,要使用 “jms” 架构中的元素,需要在 Spring XML 配置文件的顶部添加以下前缀。以下代码片段中的文本引用了正确的架构,这样 “jms” 命名空间中的元素就可以使用了:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jms="http://www.springframework.org/schema/jms"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jms
https://www.springframework.org/schema/jms/spring-jms.xsd">
<!-- 这里是 Bean 定义 -->
</beans>
# 1.3、使用 <context:mbean-export/>
此元素的详细信息在基于注解的 MBean 导出配置中介绍。
# 1.4、“cache” 架构
可以使用 “cache” 元素来启用对 Spring 的 @CacheEvict
、@CachePut
和 @Caching
注解的支持。它还支持基于 XML 的声明式缓存。详细信息请参阅启用缓存注解和基于 XML 的声明式缓存。
要使用 “cache” 架构中的元素,需要在 Spring XML 配置文件的顶部添加以下前缀。以下代码片段中的文本引用了正确的架构,这样 “cache” 命名空间中的元素就可以使用了:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
https://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- 这里是 Bean 定义 -->
</beans>