轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

  • Spring

    • 基础

    • 框架

      • Spring容器初始化过程和Bean生命周期探究
      • Spring容器:从依赖管理到注解配置全解析
      • Spring事件探秘
      • Spring AOP的应用
      • Spring 事务管理
      • Spring中的资源访问:Resource接口
      • Spring中的验证、数据绑定和类型转换
      • Spring表达式语言(SpELl)
      • Spring中的属性占位符
      • Spring数据缓冲区与编解码器详解
      • Spring对于原生镜像和AOT的支持
      • Spring中的数据访问:JDBC、R2DBC、ORM、Object-XML
      • Spring中的Web访问:Servlet API支持
      • Spring中的Web访问:WebSocket支持
      • Spring中的Web访问:响应式栈 WebFlux
      • Spring中的集成测试与单元测试
      • Spring与多种技术的集成
        • 一、客户端
          • 1、RestClient
          • 1.1、创建 RestClient
          • 1.2、使用 RestClient
          • a、请求 URL
          • b、请求头和请求体
          • c、获取响应
          • d、错误处理
          • e、交换
          • 1.3、消息转换
          • a、视图
          • b、多部分数据
          • 1.4、客户端请求工厂
          • 2、WebClient
          • 3、RestTemplate
          • 3.1、初始化
          • 3.2、主体
          • 3.3、从 RestTemplate 迁移到 RestClient
          • 4、接口
          • 4.1、方法参数
          • 4.2、自定义参数解析器
          • 4.3、返回值
          • 4.4、同步返回值(如RestClient和RestTemplate)
          • a、响应式返回值(如WebClient)
          • 4.5、错误处理
          • a、使用RestClient
          • b、使用WebClient
          • c、使用RestTemplate
        • 二、JMS(Java 消息服务)
          • 1、注意
          • 2、使用 Spring JMS
          • 2.1、使用 JmsTemplate
          • 2.2、连接
          • a、缓存消息资源
          • b、使用 SingleConnectionFactory
          • c、使用 CachingConnectionFactory
          • 2.3、目标管理
          • 2.4、消息监听器容器
          • a、使用 SimpleMessageListenerContainer
          • b、使用 DefaultMessageListenerContainer
          • 2.5、事务管理
          • 3、发送消息
          • 4、使用消息转换器
          • 5、使用 SessionCallback 和 ProducerCallback
          • 6、接收消息
          • 6.1、同步接收
          • 6.2、异步接收:消息驱动的 POJO
          • 6.3、使用 SessionAwareMessageListener 接口
          • 6.4、使用 MessageListenerAdapter
          • 6.5、事务中处理消息
          • 7、对JCA消息端点的支持
          • 7.1、Java代码示例
          • 7.2、XML配置示例
          • 7.3、Java代码示例
          • 7.4、XML配置示例
          • 8、注解驱动的监听器端点
          • 8.1、启用监听器端点注解
          • 8.2、编程式端点注册
          • 8.3、带注解的端点方法签名
          • 8.4、响应管理
          • 9、JMS命名空间支持
        • 三、JMX
          • 1、是什么?
          • 2、将 Bean 导出到 JMX
          • 3、创建 MBeanServer
          • 4、重用现有的 MBeanServer
          • 5、延迟初始化的 MBean
          • 6、的自动注册
          • 7、控制注册行为
          • 8、控制 Bean 的管理接口
          • 8.1、使用 MBeanInfoAssembler API
          • 8.2、使用源码级元数据:Java 注解
          • 8.3、JMX 注解
          • 8.4、使用 AutodetectCapableMBeanInfoAssembler 接口
          • 8.5、使用 Java 接口定义管理接口
          • 8.6、使用 MethodNameBasedMBeanInfoAssembler
          • 9、控制 Bean 的 ObjectName 实例
          • 9.1、从属性文件读取 ObjectName 实例
          • 9.2、使用 MetadataNamingStrategy
          • 9.3、配置基于注解的 MBean 导出
          • 10、使用 JSR - 160 连接器
          • 10.1、服务器端连接器
          • 10.2、客户端连接器
          • 10.3、通过 Hessian 或 SOAP 进行 JMX 通信
          • 11、通过代理访问 MBean
          • 12、通知
          • 12.1、注册通知监听器
          • 12.2、发布通知
          • 13、更多资源
        • 四、电子邮件
          • 1、库依赖
          • 2、使用方法
          • 2.1、基本的 MailSender 和 SimpleMailMessage 使用方法
          • 2.2、使用 JavaMailSender 和 MimeMessagePreparator
          • 3、使用 JavaMail 的 MimeMessageHelper
          • 3.1、发送附件和内联资源
          • a、附件
          • b、内联资源
          • 3.2、使用模板库创建电子邮件内容
        • 五、任务执行与调度
          • 1、TaskExecutor 抽象
          • 1.1、TaskExecutor 类型
          • 1.2、使用 TaskExecutor
          • 2、TaskScheduler 抽象
          • 2.1、Trigger 接口
          • 2.2、Trigger 实现
          • 2.3、TaskScheduler 实现
          • 3、调度和异步执行的注解支持
          • 3.1、启用调度注解
          • 3.2、@Scheduled 注解
          • 3.3、响应式方法或 Kotlin 挂起函数上的 @Scheduled 注解
          • 3.4、@Async 注解
          • 3.5、使用 @Async 进行执行器限定
          • 3.6、使用 @Async 进行异常管理
          • 4、task 命名空间
          • 4.1、scheduler 元素
          • 4.2、executor 元素
          • 4.3、scheduled-tasks 元素
          • 5、表达式
          • 5.1、宏
          • 6、使用 Quartz 调度器
          • 6.1、使用 JobDetailFactoryBean
          • 6.2、使用 MethodInvokingJobDetailFactoryBean
          • 6.3、使用触发器和 SchedulerFactoryBean 编排作业
        • 六、缓存抽象
          • 1、章节概要
          • 2、理解缓存抽象
          • 2.1、缓存与缓冲区的区别
          • 3、基于声明式注解的缓存
          • 3.1、@Cacheable 注解
          • a、默认键生成
          • b、自定义键生成声明
          • c、默认缓存解析
          • d、自定义缓存解析
          • e、同步缓存
          • f、使用 CompletableFuture 和响应式返回类型进行缓存
          • g、条件缓存
          • h、可用的缓存 SpEL 评估上下文
          • 3.2、@CachePut 注解
          • 3.3、@CacheEvict 注解
          • 3.4、@Caching 注解
          • 3.5、@CacheConfig 注解
          • 3.6、启用缓存注解
          • 3.7、方法可见性和缓存注解
          • 3.8、使用自定义注解
          • a、自定义注解和 AspectJ
          • 4、(JSR - 107) 注解
          • 5、功能概述
          • 6、启用 JSR - 107 支持
          • 7、基于声明式 XML 的缓存
          • 8、配置缓存存储
          • 8.1、基于 JDK ConcurrentMap 的缓存
          • 8.2、基于 Ehcache 的缓存
          • 8.3、缓存
          • 8.4、基于 GemFire 的缓存
          • 8.5、- 107 缓存
          • 8.6、处理无后端存储的缓存
          • 9、接入不同的后端缓存
          • 10、如何设置TTL/TTI/淘汰策略等功能?
        • 七、可观测性支持
          • 1、生成的观察信息列表
          • 2、观察概念
          • 3、配置观察信息
          • 3.1、使用自定义观察约定
          • 4、@Scheduled 任务检测
          • 5、消息检测
          • 5.1、低基数键
          • 5.2、高基数键
          • 5.3、消息发布检测
          • 5.4、消息处理检测
          • 6、服务器检测
          • 6.1、应用程序
          • 6.2、低基数键
          • 6.3、高基数键
          • 6.4、响应式应用程序
          • 6.5、低基数键
          • 6.6、高基数键
          • 7、客户端检测
          • 7.1、RestTemplate
          • 7.2、低基数键
          • 7.3、高基数键
          • 7.4、RestClient
          • 7.5、低基数键
          • 7.6、高基数键
          • 7.7、WebClient
          • 7.8、低基数键
          • 7.9、高基数键
          • 8、应用程序事件和 @EventListener
        • 八、检查点恢复
          • 1、运行中应用程序的按需检查点/恢复
          • 2、启动时的自动检查点/恢复
        • 九、CDS
          • 1、创建 CDS 存档
          • 2、使用存档
        • 十、附录
          • 1、XML 架构
          • 1.1、“jee” 架构
          • a、<jee:jndi-lookup/>(简单示例)
          • b、<jee:jndi-lookup/>(单个 JNDI 环境设置)
          • c、<jee:jndi-lookup/>(多个 JNDI 环境设置)
          • d、<jee:jndi-lookup/>(复杂示例)
          • e、<jee:local-slsb/>(简单示例)
          • f、<jee:local-slsb/>(复杂示例)
          • g、<jee:remote-slsb/>
          • 1.2、“jms” 架构
          • 1.3、使用 <context:mbean-export/>
          • 1.4、“cache” 架构
      • Spring框架版本新特性
    • Spring Boot

    • 集成

  • 其他语言

  • 工具

  • 后端
  • Spring
  • 框架
轩辕李
2024-07-28
目录

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 文件:

  • Jakarta Mail (opens new window) 库。

该库可在网上免费获取,例如在 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) {...}
  1. 指定 anotherCacheManager。

也可以像替换 键生成 那样完全替换 CacheResolver。每个缓存操作都会请求进行解析,这样实现就可以根据运行时参数实际解析要使用的缓存。以下示例展示了如何指定 CacheResolver:

@Cacheable(cacheResolver="runtimeCacheResolver") // (1)
public Book findBook(ISBN isbn) {...}
  1. 指定 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) {...}
  1. 使用 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) {...}
  1. 使用 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)
  1. 在 @Cacheable 上设置条件。

除了 condition 参数,还可以使用 unless 参数来阻止将值添加到缓存中。与 condition 不同,unless 表达式在方法调用之后进行计算。扩展前面的示例,也许我们只想缓存平装书,如下所示:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") // (1)
public Book findBook(String name)
  1. 使用 unless 属性来排除精装书。

缓存抽象支持 java.util.Optional 返回类型。如果 Optional 值存在,则将其存储在关联的缓存中;如果 Optional 值不存在,则将 null 存储在关联的缓存中。#result 始终引用业务实体,而不是支持的包装器,因此前面的示例可以重写如下:

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

注意,#result 仍然引用 Book,而不是 Optional<Book>。由于它可能为 null,因此我们使用 SpEL 的 安全导航运算符。

# 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)
  1. 使用 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) {...}
}
  1. 使用 @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)
  1. 为此操作自定义工厂。

注意:对于所有引用的类,Spring 会尝试查找具有给定类型的 Bean。如果存在多个匹配项,则会创建一个新实例,并且该实例可以使用常规的 Bean 生命周期回调,例如依赖注入。

键由 javax.cache.annotation.CacheKeyGenerator 生成,其作用与 Spring 的 KeyGenerator 相同。默认情况下,所有方法参数都会被考虑用于生成键,除非至少有一个参数使用 @CacheKey 注解标注。这与 Spring 的自定义键生成声明类似。例如,以下两个操作的效果相同,一个使用 Spring 的抽象,另一个使用 JCache:

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

@CacheResult(cacheName="books")
public Book findBook(@CacheKey ISBN isbn, boolean checkWarehouse, boolean includeUsed)

你也可以像指定 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>
编辑 (opens new window)
上次更新: 2025/03/28
Spring中的集成测试与单元测试
Spring框架版本新特性

← Spring中的集成测试与单元测试 Spring框架版本新特性→

最近更新
01
Spring Boot版本新特性
09-15
02
Spring框架版本新特性
09-01
03
Spring Boot开发初体验
08-15
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式